An end-to-end encrypted chat where your wallet is your account.
Sign in with MetaMask, befriend people on chain and chat; messages are encrypted in your browser and the server only ever stores ciphertext.
Live demo · Try it · How it works · Run locally · The registry contract
Last updated: 2026-10-23 · Live on Vercel (free), contracts on the Hoodi testnet · rework in progress (see roadmap)
- About
- Try it live
- Features
- Why it exists
- How it works
- Engineering quality
- Run it locally
- Environment variables
- Deployment
- Status and roadmap
- Credits
Most chat apps keep your account and your messages on a company's servers. Node Connect keeps your identity and friendships in a smart contract instead: your wallet address is your account, a friendship needs a request and an acceptance recorded on chain, and the public key that encrypts messages to you is published there. The messages themselves travel through a small backend (Vercel Functions and a Neon database) that stores only ciphertext. The key is derived in your browser from one wallet signature, so the server, the database and the chain never see a message in the clear.
Rewards are being rebuilt as a real ERC-20 token (TOK) with limits per day. The token contract is deployed and the leaderboard reads balances from it, but mining is not wired into the site yet, so every balance is zero for now.
Open https://node-connect-sigma.vercel.app in a browser with the MetaMask extension. Creating an account and answering friend requests are transactions on the Hoodi testnet, so the account needs a little Hoodi ETH, which is free from a faucet such as the Google Cloud faucet or the PoW faucet. Sending and reading messages costs no gas.
Things to try in the first two minutes (use two MetaMask accounts, one per browser profile):
- Connect wallet and approve the network switch: the app adds Hoodi to MetaMask if it is missing.
- Create an account with a username: MetaMask asks you to sign a text that creates your chat key, then to confirm one transaction that stores your name and public key.
- Open Find people and send a request; the other account sees it there and accepts.
- Open the chat, choose Unlock messages (two signatures, no gas) and send a message: it shows as sending, then lands in the thread. The other side sees it within a few seconds.
- Open the Leaderboard to see everyone and their TOK balance.
![]() Registered users you can add, read from the contract (how friendships work) |
![]() Everyone ranked by tokens (the reward rules) |
Screenshots: the production build of this commit on a local Hardhat chain seeded with demo accounts (alice, bob, carol, dave, erin), 1440x900, 2026-10-21. The live site shows the same screens for Hoodi accounts.
- Wallet sign-in: MetaMask connects the account; the app switches MetaMask to the configured network and adds it when missing. The API uses Sign-In with Ethereum (EIP-4361) and an httpOnly cookie.
- On-chain profiles and friendships: a username and an encryption public key per address in the
Registrycontract; a friendship needs a request and an acceptance, and can be declined, removed or blocked. - End-to-end encrypted chat: X25519 keys come from one wallet signature, messages are sealed in the browser (NaCl box) and the server stores base64 ciphertext only. The API accepts a message only between accepted friends, checked on chain.
- Pending and failed states: a message shows at once as sending; if the API refuses it, it stays in the thread as not sent with Retry and Discard.
- Live thread: the open conversation polls every 4 seconds while the tab is visible.
- Leaderboard of every user by TOK balance (mining arrives in a later release).
- Messages API: documented at
/api/docs(OpenAPI 3 and Swagger UI). - Local chain mode: the same app runs against a Hardhat node with test accounts.
It started as a final-year project to learn how a dApp is built end to end: a Solidity contract, its tests, a deploy to a public testnet and a frontend that talks to it through a wallet. Bringing it up to date in 2026 (Next.js 16, ethers 6, Hoodi after Holesky was retired, CI) taught the rest: a leaked deployer key does not stop mattering when the file is deleted, and putting everything on chain makes every click cost a wallet confirmation and every message public.
flowchart LR
U["Browser + MetaMask<br/>keys, encrypt, decrypt"] --> V
subgraph Vercel["Vercel (free)"]
V["Prerendered Next.js pages"]
F["Vercel Functions<br/>/api: SIWE sign-in, messages"]
end
F --> N[("Neon Postgres<br/>nonces, ciphertext only")]
F -- "areFriends (eth_call)" --> R
U -- "reads, and signed transactions<br/>for register and friend requests" --> R["Public Hoodi RPC node"]
R --> C[("Registry, NodeToken<br/>Hoodi testnet")]
What happens when you send a message:
sequenceDiagram
actor A as You
participant UI as Chat (ChatAppContext)
participant API as /api/messages
participant C as Registry (chain)
participant D as Neon
A->>UI: type a message, press Enter
UI->>UI: seal with your secret key and the friend public key
UI-->>A: bubble shown as sending
UI->>API: POST recipient, ciphertext (session cookie)
API->>C: areFriends(you, friend)
C-->>API: true
API->>D: store ciphertext
API-->>UI: 201
UI->>API: GET thread (every 4 s)
UI-->>A: opened in the browser, bubble settles
Your secret key is never sent anywhere: it is the hash of the signature MetaMask returns for a fixed
text (lib/client/crypto.js), so the same wallet rebuilds the same key pair in any browser, and the
public half is written to the Registry when you create your account. A message is
base64(nonce + box); the sender and the recipient can both open it, nobody else can.
contracts/ChatApp.sol was the whole backend of the first version. The site no longer uses it; it stays deployed, verified and tested for reference.
| Call | Who can call it | Effect | Tokens |
|---|---|---|---|
createAccount(name) |
an address without an account; the name must not be empty | stores the username | +5 to you |
addFriend(address, name) |
a registered user, for another registered user who is not already a friend and not themselves | adds the friendship to both lists | +1 to both |
sendMessage(friend, text) |
friends only | appends to the chat keyed by the hash of both addresses | +1 to both |
readMessage(friend) |
anyone (eth_call) |
returns the chat, oldest first | - |
getAllAppUser(), getMyFriendList(), getUsername(address) |
anyone (eth_call) |
read accounts and friends | - |
mineMessageSafe(friend, nonce) |
anyone | +1 when keccak(chat, last message, nonce) is below a fixed target; not used by the UI |
+1 to the caller |
Tokens are a balance kept inside the contract (minerReward), not an ERC-20 token, so they do not
show up in a wallet.
Four contracts replaced ChatApp.sol. The site uses Registry (profiles, keys, friendships) and
NodeToken (balances) since 2026-10-23; ChatAnchor and Mining wait for the mining release. All
source is verified on Blockscout.
| Contract | Address | What it does |
|---|---|---|
NodeToken (TOK) |
0xC6a2...09b0 |
ERC-20, hard cap of 21M, minted only by Mining, no admin |
Registry |
0x6c03...1802 |
profiles with an encryption key; friend request, accept, decline, remove, block |
ChatAnchor |
0x7604...f4cF |
daily epochs; the relayer posts one Merkle root of activity per finished epoch |
Mining |
0xcB3e...661f |
claim TOK with a Merkle proof of activity plus a proof-of-work nonce |
Mining rules: at least 5 messages to one accepted friend in the epoch; the nonce hash covers the
caller, so nobody can reuse another's work; one claim per user per epoch and at most 100 per epoch;
accounts must be a day old; 50 TOK per claim, halved every 90 epochs; the target retargets from the
previous epoch's claims (aim: 10) within a factor of 16; only the epoch just ended is claimable;
every failure reverts. Deployer and relayer are separate wallets (addresses in
deployments/hoodi.json); the relayer is trusted to post true roots
until the batch is published for recomputation (iteration 2).
| Layer | Technology |
|---|---|
| Smart contract | Solidity 0.8.19, Hardhat 2 (compile, local node, tests, deploy) |
| Frontend | Next.js 16 (pages router), React 18, CSS Modules, tweetnacl for the encryption |
| Backend | Vercel Functions (Next.js pages/api), Sign-In with Ethereum (siwe), httpOnly cookie sessions (iron-session), Neon Postgres (@neondatabase/serverless), OpenAPI 3 with Swagger UI |
| Chain access | ethers 6: reads through the public RPC, transactions through MetaMask (BrowserProvider); the server reads friendships with a JsonRpcProvider |
| Network | Ethereum Hoodi testnet (chain 560048); a local Hardhat node (chain 1337) for development |
| Tests and checks | Hardhat tests (Mocha, Chai), API tests (node:test), ESLint (Next.js rules), an ABI freshness check, crypto round-trip tests |
| CI and hosting | GitHub Actions (CI on every push, daily uptime check), Vercel Hobby |
| Local setup | Node 22 pinned with mise |
- Identity on chain, messages off chain. Friendships and public keys need to be tamper-proof and
readable by anyone, so they live in
Registry; message text would be public and costs gas on chain, so it goes through the API as ciphertext. Trade-off: the server can drop or delay a message and sees who talks to whom and when; it cannot read one. - Keys from a wallet signature, not stored. Nothing secret is saved in a browser or on a server; the price is a signature prompt per page load before messages open. The app compares the derived public key with the one registered and refuses to continue if they differ (a wallet that signs the same text differently each time would otherwise lock you out silently).
- The server checks friendships on chain. One
areFriendsread per send, a positive answer cached for 60 seconds. A failed read answers 503 and stores nothing; a stale yes lasts at most a minute after a friendship ends. - Polling, not websockets. Vercel Functions cannot hold a socket open on the free plan; a 4 second poll while the tab is visible is enough for a chat of this size.
- Configuration is checked in one place at build time.
Context/config.jsstops the build when a contract address, the start block or the network is missing or malformed. - The contract ABIs are checked-in files, kept fresh by CI. The frontend imports
Context/Registry.jsonandContext/NodeToken.json; CI fails when they differ from the compiled contracts.
Project structure
contracts/ Registry, NodeToken, ChatAnchor, Mining; ChatApp.sol is the legacy version
test/ contract tests (Hardhat)
scripts/deploy.js deploys the contracts; export-abi.js copies the ABIs to Context/
lib/client/crypto.js key derivation, encrypt and decrypt (browser and Node)
lib/server/ the API: sign-in, messages, chain reader, Neon store, OpenAPI
api-tests/ API and crypto tests (node:test)
Context/config.js reads and checks every environment variable (the only place that does)
Context/networks.js networks MetaMask can be switched to (hoodi, sepolia, localhost)
Context/ChatAppContext.js app state: profiles and friendships from the Registry, keys, session, thread
Utils/apiFeature.js MetaMask connection, contract instances, event reads
Utils/chatApi.js calls to /api (sign-in, send, read)
Components/ NavBar, Avatar, AccountDialog, EmptyState, Notice, Spinner
pages/ / (chat), /alluser (find people and requests), /leaderboard, /api
.github/workflows/ ci.yml (lint, tests, ABI check, build), uptime.yml (daily)
| Area | What is in place |
|---|---|
| Tests | 49 Hardhat tests, all passing on 2026-10-18: the 9 of ChatApp, plus the rework contracts (claim, replay, binding to the sender, cap, retarget, halving, every revert; registry and anchor rules; token cap and minter) |
| CI | GitHub Actions on every push: ESLint, the contract tests, Slither (0 findings, the timestamp detector excluded because epochs are time-based), a check that Context/Registry.json and Context/NodeToken.json match the compiled contracts, and the production build |
| Uptime | A daily workflow requests /, /alluser and /leaderboard on the live site and fails unless each answers 200 with the app |
| API | Sign-In with Ethereum (EIP-4361): single-use nonces that expire in 5 minutes, message domain and chain checked, session in an httpOnly SameSite=Lax cookie; the messages API accepts only base64 ciphertext, caps it at 4096 characters and rate limits 30 messages per minute per address; every response carries a request id and JSON logs hold no message content. every send is checked against the Registry (403 for a non-friend, 503 when the chain cannot be read); 17 API tests pass against Neon and 26 tests (API plus 6 crypto) against the in-memory store (2026-10-23) |
| Security | No secrets in the frontend (only public NEXT_PUBLIC_ values); the deployer key lives only in a local .env; security headers from vercel.json (nosniff, DENY framing, referrer and permissions policies) |
| Measured | Lighthouse on the live site, 2026-10-12 (mobile / desktop): performance 89-90 / 99, accessibility 100 / 100, best practices 100 / 100, SEO 100 / 100 |
| Dependencies | npm audit --omit=dev (what ships to the browser) reports no vulnerabilities; the remaining findings are in Hardhat's development toolchain |
Run them:
npm run lint # ESLint
npm test # contract tests
npm run test:api # API tests (add STORE=neon DATABASE_URL=... to run against Neon)
npm run slither # static analysis (pip install slither-analyzer solc-select)
npm run build # production buildWorks on Windows (Git Bash), macOS and Linux. You need Git, Node 22 (pinned in mise.toml;
mise installs it) and the MetaMask browser extension.
git clone https://github.com/A-RYAN-1/node-connect.git
cd node-connect
mise install # skip if you manage Node another way
npm ci
cp .env.example .env # the defaults point at the public Hoodi contract
npm run devOpen http://localhost:3000, connect MetaMask and approve the switch to Hoodi. Writes need a little
Hoodi ETH from a faucet (see Try it live).
On a local chain (no testnet ETH, no MetaMask warning):
npm run node # terminal 1: Hardhat node on http://127.0.0.1:8545 with funded test accounts
npm run deploy-local # terminal 2: deploys the contract and prints its addressIn .env, set NEXT_PUBLIC_REGISTRY_ADDRESS and NEXT_PUBLIC_TOKEN_ADDRESS to the printed addresses, NEXT_PUBLIC_REGISTRY_START_BLOCK=0 and NEXT_PUBLIC_NETWORK=localhost,
run npm run dev, and import one of the test accounts npm run node prints into MetaMask. Those
accounts and keys are public and only for the local chain.
Deploy your own contract to Hoodi
- Create a fresh MetaMask account that will only ever hold testnet ETH, and fund it from a Hoodi faucet.
- Put its private key in
.envasDEPLOYER_PRIVATE_KEY(optionally an Alchemy or Infura URL asHOODI_RPC_URL). - Run
npm run deployand setNEXT_PUBLIC_REGISTRY_ADDRESS,NEXT_PUBLIC_TOKEN_ADDRESSandNEXT_PUBLIC_REGISTRY_START_BLOCKto what it prints. - If you changed a contract, run
npm run abiso the frontend uses the new ABI.
Troubleshooting
- The build stops with a config error.
Context/config.jschecks the variables; copy.env.exampleto.env(or set them on the host) and build again. - MetaMask shows the wrong network. Click Connect Wallet again and approve the network switch.
- A transaction fails with "insufficient funds". The account needs a little Hoodi ETH.
- Local chain: MetaMask reports a nonce error after restarting
npm run node. The chain was reset; in MetaMask, Settings, Advanced, clear the activity data for that account. npm cifails on Node 18 or 20. The project needs Node 22 (mise install).
All of them are listed with comments in .env.example.
| Variable | What it is | Needed |
|---|---|---|
NEXT_PUBLIC_REGISTRY_ADDRESS, NEXT_PUBLIC_TOKEN_ADDRESS |
Addresses of the deployed Registry and NodeToken contracts. Public. |
app (build time) |
NEXT_PUBLIC_REGISTRY_START_BLOCK |
Block the Registry was deployed in; event reads start there. Public. | app (build time) |
NEXT_PUBLIC_NETWORK |
Network MetaMask is switched to: hoodi, sepolia or localhost (Context/networks.js). Public. |
app (build time) |
HOODI_RPC_URL |
Hoodi RPC URL; defaults to the free public node | npm run deploy, optional |
DEPLOYER_PRIVATE_KEY |
Key of a testnet-only deployer wallet. Secret: only in your local .env, never on the host |
npm run deploy |
DATABASE_URL |
Neon Postgres connection string. Secret. | API, npm run db:migrate |
SESSION_SECRET |
32+ random characters encrypting the session cookie. Secret. | API |
SIWE_ALLOWED_DOMAINS |
Host(s) a sign-in message may name, for example node-connect-sigma.vercel.app |
API (required in production) |
SIWE_CHAIN_ID, MESSAGES_PER_MINUTE, REGISTRY_ADDRESS, HOODI_RPC_URL |
Optional overrides (defaults: 560048, 30, the Registry in deployments/hoodi.json, the public node); the API uses the last two to check friendships |
API, optional |
NEXT_PUBLIC_ values are compiled into the public JavaScript, so they must never hold a secret.
Never commit a real .env file; only .env.example is tracked.
The site and its API run on Vercel's free Hobby plan (pages are prerendered, pages/api becomes Vercel Functions), with the messages in a free Neon Postgres database; the contracts live on the Hoodi testnet (addresses above).
| Part | Where it runs | How it deploys | Variables it needs |
|---|---|---|---|
| Website | Vercel Hobby (free), https://node-connect-sigma.vercel.app | automatically on every push to main (Vercel Git integration; checked 2026-10-16) |
NEXT_PUBLIC_REGISTRY_ADDRESS, NEXT_PUBLIC_TOKEN_ADDRESS, NEXT_PUBLIC_REGISTRY_START_BLOCK, NEXT_PUBLIC_NETWORK, DATABASE_URL, SESSION_SECRET, SIWE_ALLOWED_DOMAINS (Production and Preview) |
| Contract | Hoodi testnet | by hand, npm run deploy from a local .env; never from CI |
DEPLOYER_PRIVATE_KEY, optional HOODI_RPC_URL (local only) |
What a visitor notices (checked 2026-10-16): the site itself has no cold start; every write waits for a Hoodi block and needs test ETH from a faucet; the public RPC node is shared and can be slow at times.
Host your own copy on Vercel
- Import the repository in Vercel (framework: Next.js, no root directory).
- Create a free Neon database (Vercel Storage), run
npm run db:migrateonce with itsDATABASE_URL, and setNEXT_PUBLIC_REGISTRY_ADDRESS,NEXT_PUBLIC_TOKEN_ADDRESS,NEXT_PUBLIC_REGISTRY_START_BLOCK,NEXT_PUBLIC_NETWORK,DATABASE_URL,SESSION_SECRETandSIWE_ALLOWED_DOMAINSin the project's Environment Variables for Production and Preview. - Deploy.
vercel.jsonadds the security headers;.vercelignorekeeps env files and build output out of CLI uploads.
Reached: production baseline on 2026-10-12 (Next.js 16 and ethers 6, contract tests, lint and build in CI, checked configuration, security headers, a daily uptime check, the Lighthouse scores above), and auto-deploy on push since 2026-10-16. Checked by hand on the live site on 2026-10-13: adding a friend and sending a message on Hoodi.
Live check, 2026-10-23 (PR #4 on production): two throwaway wallets registered with their chat keys on the Registry, sent a friend request and accepted it; sending was refused with 403 before the request and after it, stored (201) after the acceptance; the friend opened the message, replied, and both sides decrypted the full history after a reload; a third signed-in address read an empty thread and was refused (403) when sending. The Neon messages table held two rows, both base64 with no plaintext, and they were deleted afterwards. The profiles nc-live-A and nc-live-B remain on chain and show in Find people. The screenshots above show the earlier interface and are refreshed in a later pass.
Known limitations
- The server sees who messages whom and when, and can drop a message; it cannot read one. Losing the wallet means losing the key: old messages cannot be opened again.
- Opening messages needs a signature per page load (the key is not stored anywhere).
- Rewards: mining is not wired in yet, so every TOK balance is zero. The legacy in-contract balance is no longer shown.
- Creating an account and answering a friend request still cost a MetaMask confirmation and gas.
- Friend and user lists are rebuilt from contract events and one read per user on every load; fine for a demo-sized network, not for thousands of users.
- No frontend or browser tests yet. The encrypted flow was checked on the live site on 2026-10-23 through the real API with two throwaway wallets (see Status); the MetaMask screens themselves were not clicked through.
Planned: the rework (agreed 2026-10-13; each step merged and deployed before the next)
- Contracts and tests (done 2026-10-18, deployed and verified from a fresh wallet; the site switches in a later step): an OpenZeppelin ERC-20 token (TOK) with a hard cap, a registry with friend requests (request, accept, decline, remove, block), deployed and verified from a fresh wallet, which should also end the MetaMask warning
- Backend on Vercel Functions and Neon (built 2026-10-23): Sign-In with Ethereum, a messages API that
stores only ciphertext, OpenAPI at
/api/openapi.jsonand Swagger UI at/api/docs; the chat UI is used by the chat UI since the encrypted-chat release. Still to do: the scheduled job that anchors each day's message hashes on chain - End-to-end encrypted chats (keys derived from one wallet signature) and the chat UI wired to the API with sending, failed and retry states (merged and live 2026-10-23, PR #4)
- Real token mining: proof of activity plus proof of work, one claim per user per day, bound to the sender, with retargeting and halving; mined in a Web Worker
- Production checks again (Slither in CI, API tests, Lighthouse), then this README
Built by Aryan Deshmukh (A-RYAN-1): the contract, its tests, the frontend, and the 2026 upgrade (Hoodi, Next.js 16, ethers 6, CI, deployment). RealFalseGod contributed the leaderboard page and the token balance in the header (October 2025).

