Introduction
ZNode Grid is private file storage. Files are encrypted in your browser, then stored as three replicas on independent storage nodes. A coordinator watches those nodes and repairs lost replicas automatically. Storage is paid for once, on-chain, and that payment funds the operators who run the nodes.
Files and their names are encrypted in your browser before upload.
Three replicas, repaired within seconds when a node fails.
Anyone can run a storage node and earn for it.
Payments and payouts follow rules enforced by a contract.
Quickstart
Storing your first file takes about two minutes.
- Get a wallet. Any browser wallet such as MetaMask, set to Robinhood Chain (chain ID
4663) with a little ETH for the payment. - Open the vault. Go to /app and click Connect wallet. Sign the login message. No gas is spent.
- Buy storage. Click Buy storage and confirm the transaction. The price is read from the contract, so you always pay the exact amount.
- Upload. Drop files onto the upload area. Each one is encrypted before it leaves your device.
- Download anywhere. Sign in from any browser with the same wallet and your files decrypt locally.
Lost track of your purchase? If your browser forgot the payment transaction, just sign in again. The backend checks the contract's hasPurchased for your wallet and restores your plan. You're never charged twice.
Run a node
A node is a small Node.js server that stores encrypted replicas on disk. One install can run several nodes, each with its own home folder.
1. Create a join token
Open /nodes, connect the wallet that should own the node and receive rewards, enter a name and capacity, and click Create join token. Tokens are single-use and expire after 24 hours.
2. Start the node
$ cd usernode $ npm install $ node server.js --token znj_… --port 4001 --capacity 5 --home ./nodes/home-server
On first start the node:
- creates its identity key in
<home>/wallet.json; - signs a join request with your token;
- passes a contact check, where the coordinator calls the node's
/healthand confirms it reports the same identity; - saves its assigned ID in
<home>/node.json.
Later starts need no token: node server.js --port 4001 --home ./nodes/home-server.
| Flag | Env var | Default | Purpose |
|---|---|---|---|
--token | JOIN_TOKEN | – | One-time join token (first start only) |
--port | PORT | 4001 | Port the node listens on |
--home | NODE_HOME | usernode/ | Identity, config and data folder |
--capacity | CAPACITY_GB | 1 | Storage offered to the network |
--url | PUBLIC_URL | http://localhost:<port> | Address the coordinator uses to reach you |
--coordinator | COORDINATOR | http://localhost:5000 | Coordinator address |
3. Retiring a node
Use Graceful exit in the console. The node stops receiving new data, its replicas are copied to other nodes, and it shuts itself down once it's empty. If no spare node is available, the exit pauses and can be retried.
Architecture
| Component | Responsibility | Can see |
|---|---|---|
| Browser | Derives your key, encrypts and decrypts files and metadata | Everything, for you only |
Backend :4000 | Wallet sign-in, payment verification, encrypted file index, repair bookkeeping | Ciphertext, encrypted names, which nodes hold which file |
Coordinator :5000 | Node admission and auth, placement, health checks, repair, reward metering, settlement | Opaque blobs and their sizes |
| Storage nodes | Store and serve replicas; only accept requests from the coordinator | Random bytes |
| StorageIncentive | Takes storage payments, holds the reward pool, pays operators | Public on-chain state |
Upload path
Browser encrypts → backend verifies your session and payment → coordinator picks the 3 least-loaded online nodes (different operators first) → copies are written in parallel → backend records which nodes hold the file.
Repair path
Nodes heartbeat every 5 seconds. After about 15 seconds of silence a node is marked offline and the coordinator asks the backend to repair it. Every replica it held is copied from a healthy replica onto a new node, and the file index is updated.
Security model
- Keys. A random 32-byte vault secret, created in your browser, encrypts every file with AES-256-GCM. The backend stores it only in wrapped form, wrapped with a key your browser derives with HKDF-SHA-256.
- Metadata. File names, types, sizes and dates are encrypted too. The server sees only an opaque ID.
- Node isolation. Nodes accept data requests only with the coordinator's session token, and validate file IDs to prevent path traversal.
- Node identity. Every node signs its join and login challenges with its own key. A node cannot claim another node's ID.
ZNode Grid is an MVP. The wrapping key is currently derived from your wallet address rather than from a wallet signature, so whoever operates the backend could unwrap vault keys; storage nodes cannot. There are also no storage audits yet (proofs that a node still holds data), and the contract has unit tests but no external audit. Don't store anything irreplaceable that you have no other copy of.
Payments
Storage is a one-time purchase made by calling purchaseStorage() with exactlystoragePrice. The contract rejects any other amount and any second purchase from the same wallet.
How the backend verifies a payment
- The server's RPC and the transaction are both on chain
4663. - The transaction succeeded and has the required confirmations.
- It was sent to the StorageIncentive address by the signed-in wallet.
- Its call data is exactly
purchaseStorage(). - Its value equals the configured price exactly.
- The contract emitted a matching
StoragePurchasedevent.
Each transaction hash can be used once. If the hash is missing or stale, the backend falls back to the contract's hasPurchased(wallet), read at a confirmed block.
See the live state and every function on the contracts page.
Node rewards
The coordinator meters three things for every node while it's online:
| Source | Measured as | Configured rate |
|---|---|---|
| Storage | Bytes held × time | 0.0001 ETH per GB-hour |
| Egress | Bytes served on downloads | 0.0005 ETH per GB |
| Uptime | Time online and heartbeating | 0.00001 ETH per hour |
| Minimum claim | Pending earnings per operator | 0.0001 ETH |
Getting paid
- Register your operator wallet once with
registerNode(). - Claim. The coordinator, acting as the contract's settler, calls
rewardNode()to move your unclaimed earnings from the reward pool to your balance. A claim is capped at what the pool holds, and the rest stays unclaimed. - Withdraw your balance to your wallet at any time with
withdraw().
HTTP API
Backend
| Method | Path | Purpose |
|---|---|---|
| POST | /nonce | Get a login nonce for a wallet |
| POST | /login | Exchange a signed nonce for a session |
| GET | /payment-status | Whether the wallet has storage (restores on-chain purchases) |
| POST | /payment | Verify a purchase; txHash optional |
| POST | /upload | Upload an encrypted file and metadata |
| GET | /files | List your encrypted file index |
| GET | /download/:id | Stream a file from any live replica |
| DELETE | /files/:id | Delete a file from every node |
Authenticated calls send the session in the x-session header.
Coordinator: operators
| Method | Path | Purpose |
|---|---|---|
| POST | /operator/nonce, /operator/login | Wallet sign-in for operators |
| GET | /operator/overview | Nodes, earnings, tokens and payouts |
| POST | /operator/join-tokens | Create a one-time join token |
| POST | /operator/nodes/:id/exit | Start a graceful exit |
| POST | /operator/payouts | Settle unclaimed earnings on-chain |
| GET | /network | Public network totals and rates |
Operator calls send the session in the x-operator-session header.
Troubleshooting
“Switch your wallet to Robinhood Chain”
Add the network in your wallet: chain ID 4663, RPC https://rpc.mainnet.chain.robinhood.com, currency ETH.
Payment says “not confirmed yet”
Wait a few seconds and click Buy storage again. The same transaction is re-checked and you won't be charged twice.
A node says “Contact check failed”
The coordinator couldn't reach the node at its --url. Make sure the port is reachable and the URL is correct.
Claim is disabled
Register your payout wallet first, make sure unclaimed earnings exceed the minimum, and check that settlement is configured on the coordinator.