The privacy layer for tokenized stocks on Robinhood Chain.
Wall Street sees everything. RhMask sees nothing.
rhmask.org · @RHmask_ · Telegram · GitHub · npm
Open the app · Pay privately · Docs · Whitepaper · Product · Architecture · Tokenomics · Extension · Sandbox
Tokenized stocks put the market on a public ledger. Every entry, every exit, every balance is readable by anyone with an explorer. RhMask gives holders three things: a way to receive unseen, a way to trade unseen, and a vault that pays stakers in real stock tokens.
Three surfaces, one core: the web app (this repository), a browser extension that drops a fresh stealth address into any address field on any site, and a command line that does the key work with no browser at all.
npx rhmask # not published yet; build it with npm run build:cli| Surface | What it does | State |
|---|---|---|
| Ghost Receive | One meta-address, a fresh unlinkable address per payment. ERC-5564 stealth addresses derived in the browser, keys encrypted at rest. | beta |
| Private Pay | Request a payment as a QR, pay to a one-time address from any wallet, hand back a receipt QR, claim and sweep. Three scans, zero servers. | beta |
| Mask Swap | Private fills instead of public order books. Venue name and flat fee printed before you send. Non-custodial. | beta · dry quotes |
| Blue Chip Vault | Stake $MASK; protocol revenue buys a basket of stock tokens and streams it to stakers. |
planned |
| Proof Ledger | Every payout listed with its transaction hash. If it is not on the ledger, it did not happen. | planned |
rhmask CLI |
Nine commands. Five never open a socket; none can sign or broadcast. Prints a scannable payment-request QR into the terminal. | built, unpublished |
Honest status. Anything marked
plannedhas no on-chain effect yet. Nothing on the site is estimated or back-filled. The live status table is on the landing page.
sequenceDiagram
autonumber
participant R as Recipient
participant P as Payer
participant C as Robinhood Chain
R->>R: Generate keys once, publish meta-address
R->>P: Payment request QR (/pay?to=st:eth:0x…)
P->>P: Derive one-time stealth address in the browser
P->>C: Send ETH or a stock token from any wallet
P->>R: Receipt QR (one-time address + ephemeral key + view tag)
R->>R: Viewing key recognises it, spending key unlocks it
R->>C: Sweep to any address, signed in the page
Nothing in this flow touches a server. The receipt QR carries the ERC-5564 announcement until the announcer contract ships, and the format maps one-to-one onto the on-chain event so the scanner can replace the QR without a format change.
npm run check:onchain runs read-only against Robinhood Chain and simulates from real holders. Nothing is signed or broadcast.
| Check | Result |
|---|---|
| Stock token registry | 10 live ERC-20s, symbols and decimals match, all issuer-owned upgradeable proxies |
| Stealth receive | A fresh one-time address receives NVDA, SPY, TSLA, AAPL, MSFT in simulation |
| Vault feasibility | Every basket token transfers to a contract address, approve works, nothing paused |
| Sweep error path | An empty stealth address fails cleanly, and the UI says why in a sentence |
What is hidden, what is not
Hidden: the link between you and a receiving address (stealth addresses); your order from public order books and mempools as a visible swap.
Also hidden: your IP from the chain’s node operator. The browser talks to this app’s own origin and the server forwards, so the node sees us, not you.
Not hidden: on-chain settlement itself; the venue filling an order sees the deposit and the receiving address; your IP from our own host, which logs it like any web server. Point NEXT_PUBLIC_RHC_RPC_URL at your own node and neither of us sees it.
We say this on every screen where it matters. Never "anonymous", never "untraceable".
| Layer | Choice | Why |
|---|---|---|
| Framework | Next.js 16, React 19, Tailwind v4 | Route handlers for the small server side, static landing, one deploy |
| Chain access | viem | Typed, tree-shakeable, no wallet SDK, no telemetry |
| Stealth crypto | @noble/curves + @noble/hashes |
Audited pure-JS secp256k1 and keccak, runs in the browser |
| Key storage | WebCrypto PBKDF2-SHA256 + AES-256-GCM | Encrypted at rest, decrypted only in memory |
| QR | qrcode (SVG) + native BarcodeDetector with jsqr fallback |
Renders on the first frame, scans on every browser |
| Wallet | EIP-1193 injected provider | No account linkage, no third-party connect modal |
| Hosting | Any Node host | /api/rpc pass-through keeps the app working where the chain's RPC domain is blocked |
npm install
cp .env.example .env.local # fill in what you have
npm run devThe app runs with no keys at all. Routes that need a key return a 503 naming the exact env var.
| Variable | Scope | Purpose |
|---|---|---|
NEXT_PUBLIC_APP_URL |
public | Canonical URL for metadata and QR links |
NEXT_PUBLIC_RHC_RPC_URL |
public | Override the browser RPC (default: same-origin /api/rpc) |
ROUTER_JWT |
server | Intent router partner key; enables live quotes |
ROUTER_FEE_RECIPIENT |
server | Address that receives the routing fee |
ROUTER_FEE_BPS |
server | Routing fee in basis points (default 30) |
ALCHEMY_API_KEY |
server | Production RPC |
NEXT_PUBLIC_* values are inlined into the browser bundle. Never put a secret behind that prefix.
| Command | What it proves |
|---|---|
npm run check |
Everything below plus the production build. This is the pre-push gate and the CI job. |
npm run check:fast |
Same without the build |
npm run check:stealth |
ERC-5564 round-trip: derive, recognise, recover, and a stranger cannot |
npm run check:payment |
Private Pay request and receipt formats round-trip, bad input is rejected |
npm run check:keycrypto |
Sealed keys open again, a wrong passphrase fails, a tampered blob fails |
npm run check:captions |
Every caption in marketing/ has its poster rendered beside it |
npm run check:cli |
The CLI works end to end, and its terminal QR still decodes |
npm run check:onchain |
Read-only mainnet check: tokens, stealth receive, vault feasibility, sweep errors |
npm run hooks:install |
Installs the git pre-push hook once per clone |
rhmask/
├─ src/
│ ├─ app/
│ │ ├─ page.tsx landing with the honest status table
│ │ ├─ app/ dashboard · Swap · Receive · Pay · Vault
│ │ ├─ pay/ landing for a scanned payment-request QR
│ │ ├─ opengraph-image.tsx social card, rendered at build time
│ │ └─ api/
│ │ ├─ health · chain · tokens · vault
│ │ ├─ quote POST private-fill quote
│ │ ├─ order/[id] GET settlement status
│ │ └─ rpc POST JSON-RPC pass-through (allow-listed)
│ ├─ components/ PayCard · ClaimCard · StealthCard · SwapCard · VaultCard · QrCode · QrScanner
│ ├─ hooks/useWallet.ts EIP-1193, no SDK
│ └─ lib/
│ ├─ stealth.ts ERC-5564 derivation and recognition
│ ├─ payment.ts request and receipt wire formats
│ ├─ keystore.ts local key store, passphrase encryption
│ ├─ transfer.ts wallet transfers, balances, pre-flight, sweep
│ ├─ qr.ts QR generation and decoding
│ ├─ chain.ts · tokens.ts · env.ts · router/
├─ cli/ `rhmask` command line · one bundled file, no runtime deps · npm run build:cli
├─ extension/ MV3 browser extension (popup, address-field chips, receipts) · npm run build:extension
├─ scripts/ stealth-check · payment-check · onchain-check · prepush-check · install-hooks
├─ contracts/ Solidity specs (vault, announcer, revenue router)
├─ docs/ product · plan · extension · architecture · tokenomics · roadmap · push rules
└─ marketing/ narrative and launch copy
| Method | Path | Notes |
|---|---|---|
GET |
/api/health |
liveness |
GET |
/api/chain |
chain params, live block number, RPC error if any |
GET |
/api/tokens |
stock token registry |
GET |
/api/router-tokens |
assets the intent router can fill, cached 10 minutes, feeds the swap picker |
POST |
/api/quote |
{ originAsset, destinationAsset, amount, recipient, refundTo, slippageBps?, dry? } |
GET |
/api/order/:depositAddress |
order status |
GET |
/api/vault |
totals, basket, payouts (all zero until the vault ships) |
POST |
/api/rpc |
JSON-RPC pass-through: read methods and eth_sendRawTransaction, batches capped, log ranges bounded |
| Chain | Robinhood Chain, id 4663 (Arbitrum Orbit L2) |
| Sandbox | Robinhood Chain Testnet, id 46630, settles to Sepolia. Both contracts live there too — see docs/TESTNET.md |
| RPC | https://rpc.mainnet.chain.robinhood.com (public, rate-limited) |
| Explorer | https://robinhoodchain.blockscout.com |
| Stock tokens | 18-decimal ERC-20s, issuer-owned beacon proxies; addresses in src/lib/tokens.ts, re-verify before wiring value |
Start with docs/ARCHITECTURE.md for the invariants and the interface rules, then
docs/PUSH_RULES.md for what a push has to pass. In short:
- Every push passes
npm run check, locally through the hook and again in CI. - One change per commit, imperative subject, no trailers of any kind.
- Human contributors only. Public text is scanned for foreign brands and attribution words before it leaves the machine.
- Nothing private is ever tracked:
internal/,.env*, keys.
A standard Next.js build on any Node host. npm run build, then npm start. Route handlers need the Node runtime, not an edge-only one, and no database is required.
Canonical domain is rhmask.org; set NEXT_PUBLIC_APP_URL=https://rhmask.org so metadata and QR links resolve. Deploys are run by hand, on purpose: this repository runs checks and nothing else.
MIT. RhMask is non-custodial software. It does not hold funds, does not provide investment advice, and does not guarantee execution, rates, or settlement times. Stock tokens are issued by a third party and may be unavailable in your jurisdiction.