Skip to content
schancelPublic

About

Monad-powered cryptomessenger, forked from Stamp/cashweb (Lotus)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Frank

Frank

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.

Why Frank

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

Why this belongs in the Ethereum ecosystem

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.

What works today

The current branch demonstrates a complete Monad testnet path:

  1. The client creates or restores a local account and publishes its public profile.
  2. The sender resolves the recipient's public messaging and payment keys.
  3. The client encrypts the message using authenticated XChaCha20-Poly1305 with ECDH-derived key material.
  4. The wallet reserves funding inputs and signs one or more stamp transactions to recipient-controlled destinations.
  5. The wallet persists the encrypted message, signed transactions, and payment state before submission.
  6. The relay validates the submission and retains/routes the encrypted envelope; payment broadcasting and chain reconciliation continue independently.
  7. 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.

Honest status

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

The original Stamp privacy construction

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:]

Architecture

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.

Run the whole demo with one command

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.

Run the local testnet demo

Requirements

  • 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 .env

Set 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.sh

The 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:8098

That 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:browser

Open 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.

Tests

Backend:

cd backend/cashweb
cargo test -p cashweb-registry
cargo build --all-targets

Wallet and protocol packages:

cd packages/wallet
yarn jest

cd ../cashweb
yarn jest

Frontend:

cd app
yarn test:unit:ci

The larger idea

Smart 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:

  1. It is addressed by cryptographic identities rather than mandatory PII.
  2. Content is encrypted end to end.
  3. Unsolicited delivery has an enforceable economic cost.
  4. 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.

Development priorities

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.

Origins and licensing

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.

Explicit directory Stage A

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.

About

Monad-powered cryptomessenger, forked from Stamp/cashweb (Lotus)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages