Private, economically spam-resistant messaging for Monad.
Paid direct messages carry real payments to their recipients. Public topic posts and votes burn value.
Frank is a Monad port and continuation of Stamp, a cryptomessenger built around a simple rule: free speech is not free. Paid direct messages carry signed on-chain stamp transactions, and the relay independently validates their payment evidence. Relay delivery and chain settlement are tracked separately.
That economic primitive makes Frank more than chat. It is a candidate transport for any Ethereum-family protocol that needs private, asynchronous negotiation before on-chain settlement: agent coordination, swaps, payment channels, threshold signing, credentials, escrow, games, and other multi-round protocols.
Warning
Frank is an early testnet prototype. It uses real cryptography and real Monad testnet transactions, but its protocol and wire formats are still changing. Do not use it with valuable keys or mainnet funds.
Most encrypted messengers still require an account from one operator. Most blockchain applications can settle a transaction but have no private, authenticated, spam-resistant channel for the negotiation that comes first.
Frank combines:
- Permissionless accounts: generate a secp256k1 identity locally without a phone number, email address, legal name, or central account issuer.
- Paid delivery: recipient stamp payments give unsolicited direct messages an economic cost; delivery and payment confirmation remain distinct states.
- Burn-weighted broadcasts: public topic posts and votes burn MON because there is no single recipient to compensate.
- Transaction-carrying messages: the raw stamp transaction travels with the off-chain message, so relays can validate and submit it directly instead of relying on a chain-wide transaction indexer to discover it.
- End-to-end encrypted content: relays route and validate delivery without receiving the plaintext.
- User-run infrastructure: Stamp's architecture lets users choose or run mailbox relays; restoring per-profile relay discovery and migration on Monad is an active design gap.
- Parallel nonce lanes: a pool of funding accounts lets unrelated outgoing stamps use separate EVM nonce sequences. Funding-account reuse is permitted; automatic balance diversification is unfinished.
- Wallet and agent primitives: the protocol client is separated from Vue/Pinia, and the repository includes a headless Qwen bot with its own Frank identity.
The distinction between payments and burns is fundamental:
direct message ── stamp payment ──▶ recipient
topic broadcast ── burn ───────────▶ unspendable address
Frank is an implementation-driven exploration of themes that have repeatedly appeared in Ethereum research:
- Vitalik's guide to stealth addresses describes recipient-controlled one-time destinations that a sender can derive without another interaction.
- The Three Transitions argues that an address must evolve into richer instructions containing payment addresses, encryption keys, and multichain information—and explicitly considers direct sender-recipient communication as part of that future.
- What I would love to see in a wallet emphasizes that wallets must preserve the privacy and decentralization properties users expect from Ethereum itself.
- Make Ethereum Cypherpunk Again calls for permissionless, censorship-resistant, auditable tools instead of new centralized empires.
These links are context, not endorsements. Frank's contribution is to combine identity, encrypted messaging, economic spam resistance, recipient payments, and federated mailbox infrastructure in a working system.
The current branch demonstrates a complete Monad testnet path:
- The client creates or restores a local account and publishes its public profile.
- The sender resolves the recipient's public messaging and payment keys.
- The client encrypts the message using authenticated XChaCha20-Poly1305 with ECDH-derived key material.
- The wallet reserves funding inputs and signs one or more stamp transactions to recipient-controlled destinations.
- The wallet persists the encrypted message, signed transactions, and payment state before submission.
- The relay validates the submission and retains/routes the encrypted envelope; payment broadcasting and chain reconciliation continue independently.
- The recipient fetches and decrypts the message, and checks the attached payments.
The same stack also supports Monad-native topic posts, burn-weighted votes, a browser UI, and headless bots including Qwen. The wallet UI exposes additional configured EVM, Bitcoin-family, and Solana networks; their balances and sending depend on the relay's configured upstream capabilities. This does not make cross-chain atomic swaps available.
┌─────────────────────────────────────┐
│ Frank client │
│ Local keys + funding account pool │
└──────────────────┬──────────────────┘
│ encrypted envelope
│ + signed stamp transactions
▼
┌─────────────────────────────────────┐
│ CashWeb relay │
│ Validate + retain + route envelope │
└──────────────────┬──────────────────┘
│ verify / broadcast / reconcile
▼
┌─────────────────────────────────────┐
│ Monad testnet │
│ Recipient payments + public burns │
└─────────────────────────────────────┘
The client also reconciles and can rebroadcast retained payment transactions. A message may carry a bundle: no single transaction hash represents every stamp or an additional stealth payment.
Frank is a port of a deeper protocol, not a finished security product. This table separates the working hackathon path from the privacy work still being restored:
| Capability | Monad status |
|---|---|
| Local account creation without required PII | Working |
| Signed Monad-native profile registration | Working |
| Encrypted direct-message content | Authenticated XChaCha20-Poly1305; no recipient-key forward secrecy or ratchet |
| Direct-message stamp pays the recipient | Working; paid to one-time stamp-child destinations rather than the recipient's known EOA |
| Topic posts and votes burn MON | Working |
| Relay-side transaction broadcast, confirmation, and verification | Working |
| Multiple sender funding accounts and nonce tracking | Implemented; account reuse is allowed and automatic diversification remains unfinished |
| Recipient one-time stamp-child destinations | Working; restored on Monad in #60 |
| Split multi-transaction stamp construction | Working; restored on Monad in #60; one payment is permitted |
Single-record domain discovery (/.well-known/cashweb.json) |
Working (#1578) |
| Cross-device join-semilattice journal, outbox, and checkpoints | Working (#1570) |
| Cross-chain atomic settlement | Not available in the demo |
For a direct message, Stamp does not pay the recipient's familiar public address. It derives a recipient-controlled one-time public key from the encrypted payload digest and the recipient's destination public key:
stamp public key = H(payload digest)·G + recipient public key
The recipient reconstructs the matching private key:
stamp private key = H(payload digest) + recipient private key (mod n)
Original Stamp then derives child destinations and splits value across funding sources. Frank retains recipient-controlled derived destinations and supports multi-transaction stamps, including payments gathered from several accounts. A single sufficient funding account is also permitted. The current canonical implementation is in monad-canonical-dm.ts and canonical-dm-stamp.ts; the equations above describe the original construction, not the current wire format. Splitting payments does not by itself unlink funding accounts or prevent address reuse.
For EVM, a derived secp256k1 public key becomes an address through:
keccak256(uncompressed_public_key[1:])[12:]
frank/
├── app/ Quasar/Vue browser and desktop client
├── packages/
│ ├── wallet/ Custody adapters, unified ChainAdapter, journal and sync
│ ├── cashweb/ Relay/protocol clients, directory discovery and transport records
│ ├── nakamoto/ Typed UTXO chain library (BTC, BCH, XEC, XPI, DOGE, LTC)
│ ├── frank-codec/ Canonical encoding and validation
│ ├── crypto-box/ Deniable authenticated encryption
│ ├── mail-gateway/ Bridged SMTP/Resend gateway, WKD and onboarding portal
│ ├── price-feeds/ Thermodynamic AVU energy oracle and price feed client
│ ├── account-recovery/ Recovery-share ceremonies
│ └── bot/ Headless bots and live-demo scripts
└── backend/
├── cashweb/ Rust registry/relay workspace
└── bitcoinsuite/ Vendored chain/protocol dependencies
Application composition selects chain adapters through an ActiveChain and unified ChainAdapter boundary (MON, ETH, SOL, XEC, BTC, BCH, XPI, DOGE, LTC, TEMPO). Canonical network identities and protocol capabilities come from the shared chain registry; UI selection and relay GET /chains discovery (including accepted_payment_networks) determine which supported networks are available. Wallet inventory, reservations, and signing remain separate from message delivery.
docs/CASHWEB-PROTOCOL-SPEC.md is the normative human protocol and
status index. docs/backend-topology.md and
docs/public-federation-plan.md retain subordinate topology and
historical transition constraints; their protobuf and exact-wire sketches are not target formats.
yarn install --frozen-lockfile
yarn demo # relay + every bot on Monad testnet, from your own .env (never committed)It creates the bot identities, starts the relay and the bots, waits until they are ready, prints
their addresses and the command to start the app, and shuts everything down on Ctrl-C. Every
variable it reads is documented in the table in packages/bot/README.md;
yarn demo:smoke checks that each bot answers.
- Node.js 24+
- Yarn 1.x
- A current Rust toolchain
- A Monad testnet JSON-RPC endpoint
Install JavaScript dependencies and create local configuration:
git clone https://github.com/schancel/frank.git
cd frank
yarn install --frozen-lockfile
cp .env.example .envSet MONAD_TESTNET_HTTP_RPC_URL in .env. The remaining example values are suitable for local testnet development; never commit secrets or funded private keys.
Start the persistent Monad development server:
backend/cashweb/run-local-monad.shThe default local config stores profiles, messages, and topics in
backend/cashweb/data/registry.rocksdb, which is gitignored and survives server restarts. It
deliberately omits [bitcoin_rpc]: legacy Lotus routes fail closed, while the Monad routes do not
require a running Lotus daemon. The Monad mailbox (direct messages) is enabled in the checked-in config, which stays secret-free:
the launcher reads MONAD_TESTNET_HTTP_RPC_URL (required) and FRANK_RELAY_NETWORK (default
monad-testnet) from .env/the environment, prints the effective non-secret values, compiles through the
repository's shared cache/slot wrapper, and validates the configuration with the production parser
first. It releases the build slot before starting the long-lived relay, which reads the config from
standard input. cashwebd-exe itself refuses to start without [registry] network or without an RPC URL for
it; see docs/backend-topology.md for the full variable list and production notes.
The dependency-free launcher regression can be run with
backend/cashweb/run-local-monad.test.sh.
For an isolated throwaway run, use the explicitly ephemeral test/demo server instead:
cargo run -p cashweb-registry --example e2e_demo_server -- 127.0.0.1:8098That command creates a fresh temporary database and deletes it when the process exits.
In another terminal, start the browser client:
cd app
set -a
source ../.env
set +a
export QCLI_MONAD_RELAY_BASE_URL=http://localhost:8098
export QCLI_MONAD_RPC_CHAIN="${MONAD_RPC_CHAIN:-monad-testnet}"
export QCLI_MONAD_STAMP_BURN_ADDRESS="$MONAD_STAMP_BURN_ADDRESS"
yarn dev:browserOpen http://localhost:8080.
The frontend receives only the public relay URL and protocol chain identifier. The relay reads and
validates MONAD_TESTNET_HTTP_RPC_URL server-side; the provider URL and its API key are never
embedded in the browser bundle.
Note
Docker definitions are present, but the Docker build has not been verified in the current development environment. The commands above are the known local path.
Backend:
cd backend/cashweb
cargo test -p cashweb-registry
cargo build --all-targetsWallet and protocol packages:
cd packages/wallet
yarn jest
cd ../cashweb
yarn jestFrontend:
cd app
yarn test:unit:ciSmart contracts are excellent at deterministic settlement, but many useful protocols are interactive before they settle. Participants need to exchange commitments, negotiate terms, acknowledge state, or privately provide evidence.
Stamp was also designed to avoid making a general-purpose blockchain indexer part of the messaging path. A message carries the transaction needed to justify its delivery; the receiver or relay can validate that supplied transaction and query only the relevant chain state. On Monad, multiple funding accounts create independent nonce lanes, allowing unrelated stamp payments to progress without sharing one account's ordered transaction queue.
Frank offers a reusable channel with four unusual properties:
- It is addressed by cryptographic identities rather than mandatory PII.
- Content is encrypted end to end.
- Unsolicited delivery has an enforceable economic cost.
- Relays are infrastructure providers, not identity issuers.
That makes the messenger a reference application for a broader primitive: private, metered interactive protocol transport for the Monad and Ethereum ecosystem.
The longer-term multichain direction separates the network that supplies the mandatory stamp from the networks and assets negotiated inside a message. Lotus could eventually serve as the stamp or “gas” layer while the same encrypted conversation coordinates atomic settlement on Monad, Ethereum, or other chains. Reaching that design requires restoring Lotus infrastructure and specifying the atomic-swap or proof protocol; cross-chain atomic settlement is not currently available in the demo.
The immediate goal is correctness, not expansive claims:
- Review the current deniable authenticated encryption and its key-compromise limits.
- Restore federated per-profile relay routing and migration.
- Separate logical message IDs from recipient-specific payload digests.
- Make deletion, checkpoints, wallet effects, accounts, and nonces safe across devices.
- Complete and verify conversation threading, group mailboxes, and cross-device recovery.
See the open issues and PLAN.md for implementation history and current constraints.
Frank is derived from Stamp and CashWeb. The application and extracted protocol packages retain their respective upstream licensing boundaries; consult the package-level documentation and source headers before redistribution.
Directory publication is opt-in and separate from profiles, the trust probe and direct-message
activation. The three routes are PUT /directory/v1/:network/:subject/head, GET on that head,
and GET /directory/v1/:network/:subject/statements/:t1. Successful bodies are exact
application/vnd.frank.cbor attestations. x-frank-directory-evidence distinguishes
fresh-current from historical; historical bytes grant no fresh routing authority.
An operator must supply [registry.directory] with an absolute clock_file containing trusted
Unix nanoseconds and a complete [[registry.directory.principals]] list (maximum 1024). Each
principal supplies network, subject, revision_zero, manifest_identity, relay_id,
relay_identity, the exact authenticated HTTPS endpoint, decimal binding_expiry_ns,
bundle_root, an external continuity_file, and explicit mode = "new" or "reopen".
These are installed trust facts, never values inferred from PUT bytes or a URL. Writable database
and continuity paths must remain outside the immutable bundle; continuity also remains outside
the database rollback domain. This Rust continuity envelope retains the full public checkpoint
and installed identity; it is not the TS demo continuity JSON representation. After enrollment,
restart with reopen; missing/corrupt continuity is operator resolution, never permission to
reset or enroll again.
The Rust backend requires a loopback bind when directory routes are enabled. In the explicit
--directory-admission /absolute/public-config.json demo mode, adding
"routeTransport":{"backendUrl":"http://127.0.0.1:18098"} starts a distinct HTTPS front after
stopping the trust probe. It uses the installed bundle's exact endpoint and certificate, reserves
the bundle listener exclusively, and preserves browser CORS headers. Run the configured Rust
backend separately and stop it gracefully after stopping the front. The original trust probe's
3s/5s limits remain intact; the actual directory front allows 70s socket/upstream inactivity,
while request-body collection stays bounded to 262144 bytes and 5s.
One native owner serves every directory operation for the owning registry, with eight pending
slots reserved before buffering bodies. Response waiting is 60s, including queue/body time;
client transport waiting is 65s. Started timeout/disconnect means outcome-unknown: the owner
continues through checkpoint completion. Shutdown closes acceptance, cancels queued jobs and
waits for the started operation; a 60s drain observation never aborts it. These are operational
budgets, not freshness extensions or completion guarantees.
packages/cashweb/relay/directory-client.ts takes an independently authenticated HTTPS fetch
function, an existing public DirectoryStore, a fresh context provider, and an external
checkpoint saver. It selects no storage platform and automatically retries nothing. Retain
preparePut's exact bytes/T1 and prior checkpoint. After an uncertain attempt, explicitly fetch
and independently admit any required catch-up chain/current, then compare exact retained
historical evidence using resolve. Historical absence while an original operation might still
run is not proof of cancellation. An old duplicate returns the actual fresh retained head; it
never resurrects that old record or calls advance again at the history cap.