Who this is for
A machine that is on anyway — a NAS, a Raspberry, a small home server, a box in a rack you already pay for — with a gigabyte or more of space you do not need, on the same LAN as a Sensmos node. Linux with Python 3, nothing to install beyond the script itself. Root is needed only to write the state file and the systemd unit.
A buyer is always another node owner: Store is internal trade between people who run nodes, not a public cloud. You never deal with the buyer directly; the backend picks sellers, moves the bytes and settles the days.
What you actually hold
Encrypted blobs. The buyer's app encrypts every file on the phone with a key derived from the buyer's wallet signature; what reaches you is ciphertext with a random id. You do not receive a key, a file name, or anything that identifies the buyer. There is no code path on your side that could decrypt — the agent is one readable file, look for yourself.
Since store-py/0.2 a small .meta file sits next to every blob: the wrapped file key, the encrypted name, the sizes and the block hashes, signed by the buyer's wallet. It changes nothing about what you can read — the key in it is sealed to the buyer and useless to anyone else. It exists so the files survive us: until now that data lived only in the backend's database, and losing that database would have meant losing the files even though the bytes were alive on your disk. The record travels with the bytes, so the second copy carries the same one.
Quick start
Run this on the machine that will hold the data. It talks to your node once, over the LAN, and from then on to the backend only.
# 1 — get the agent (single file, standard library only)
sudo curl -fsSL -o /usr/local/bin/sensmos-store.py https://sensmos.com/ext/sensmos-store.py
sudo chmod +x /usr/local/bin/sensmos-store.py
# 2 — pair through your node: its LAN address, its PIN, a label, where to keep blobs, how much to offer
sudo sensmos-store.py --pair 192.168.1.50 --pin 123456 --name "NAS in the basement" \
--dir /srv/sensmos-store --capacity 100G
# 3 — install & start it as a boot-persistent service
sudo sensmos-store.py --install-service
# 4 — watch it
journalctl -u sensmos-store -f
Pairing prints OK — attachment …, token saved to /etc/sensmos-store.json. The service log then shows:
21:14:02 store agent store-py/0.3 dir=/srv/sensmos-store capacity=100 GB used=0.00 GB
21:14:03 connected as 3f9a1c2e
From that moment you are in the pool. The first buyer shows up in the log as put 7d0b… 18.1 MB … followed by put 7d0b… done, and proofs as proof 7d0b… block 3: answered (10 MB hashed).
The node's LAN address is in your router's client list; the PIN is the node's PIN from the app. The node has to be online (connected to the backend) at the moment of pairing — it relays your request and the backend's answer.
How you get paid
| what | terms |
|---|---|
| price | 0.1 GALU per GB per copy per day — the buyer picks the size, from 1 GB up, and can change it by 1 GB later; 10 GB is 1 GALU a day per copy |
| your share | 70%; 30% returns to the reward pool |
| paid for | every day a buyer's package is pinned to you and you were there for it: with their files on your disk, your copy passed a proof; while the package is still empty, your agent reported in that day — the space is reserved either way |
| not paid for | days offline or with a failed proof; days on which the buyer had no GALU — that day counts as arrears for the buyer (uploads paused, copies released after 14 unpaid days) and you are not paid for it |
Settlement runs once an hour on the backend and books each (day, buyer, seller) once. GALU lands in your wallet's balance like any other service income.
Proofs — how the backend knows you still have the file
Three times a day per copy the backend picks a random 10 MB block of a random file you hold and sends a salt. Your agent hashes the block with the salt and answers with 32 bytes — no data moves, nothing is computed on our side. With two copies at two sellers, the two answers are compared with each other. When they disagree, or when only one copy is online, the backend asks for the raw block and checks it against the hash list the buyer supplied at upload.
Three failed proofs in a row and the copy is marked dropped: no more challenges, no more pay. There is no penalty beyond that — you simply stop earning on it.
Capacity and how sellers are chosen
--capacityis what you offer. It is not verified — a seller who declares more than they have fails proofs and earns nothing, which is the whole check.- What counts is free space in the backend's own ledger (what it has placed with you), not what your agent reports; the report every five minutes is informational.
- A buyer picks the size of their package when they buy — any whole number of gigabytes, bounded by what sellers actually have free — and grows or shrinks it a gigabyte at a time at the same sellers afterwards. Every whole free gigabyte you offer can be sold.
- Sellers are ranked by free space; the buyer's two copies always go to two different owners, and never to the buyer's own machines.
- Once chosen you are pinned: every file that buyer uploads from then on lands with you, and when they buy an extra gigabyte it has to fit at your place. They can also shrink the package, a gigabyte at a time, down to what their files take. Their package moves nowhere on its own. A buyer can close it at any time: their files are deleted from your disk, the space is free again and the payments stop with that day.
To change what you offer, edit capacity_b in /etc/sensmos-store.json and restart the service. To stop selling, stop the service — without proofs your copies are dropped within a day and you leave the pool; the blobs in --dir are yours to delete.
Network
The agent dials out to sensmos.com over TLS and keeps that one connection. Control — which file to keep, which block to prove, what to delete — always arrives over it, and that works behind CGNAT with no inbound port.
File bytes are a separate question. They travel straight between you and the file's owner whenever one of the two can be dialled:
- you can be dialled — the owner's device connects to your port and the file passes directly;
- they can be dialled — you connect to them instead;
- neither — the bytes pass through the backend, as they always did.
The backend decides which of the three, every single time, and checks reachability right before the transfer. An address that worked an hour ago proves nothing: phones change networks, router forwards disappear after a reboot. The agent never picks a route on its own.
The port it listens on (9033 by default) answers every connection with one line naming this agent. That is also how reachability is measured — the backend dials the port itself, because sellers who believe they have a public address and do not are common enough that self-reporting would be worthless. If your router speaks UPnP the port is forwarded automatically; --no-upnp if you would rather arrange it yourself, --no-direct to listen on nothing at all.
13:16:55 direct: listening on TCP 9033
13:21:55 direct_ok: Reachable from the internet at 203.0.113.9:9033.
13:24:03 direct recv 8f2a1c7d with 198.51.100.4: ok (12.4 MB)
A host that cannot be dialled keeps selling exactly as before — its bytes simply take the third route.
What you verify
Every byte you receive is hashed as it lands and checked against the record the owner signed before it ever left their device: block by block, then the whole file. A file that does not match is never written — you report the bad block and nothing reaches your disk.
This protects you, not us. A broken upload used to become your problem: it sat there, failed its daily proof, and cost you that day's pay for someone else's mistake. Now it is refused at the door. The check runs on all three routes, including copies pulled from another host.
Where you see it
In the app (1.5.60 or newer), on the card of the node that vouches for the agent, under Additions: online or not, offered and used space, copies held, proofs of the last seven days, earnings. A long press detaches the agent (its token is revoked at once). The service log has the same, line by line.
Flags
| flag | meaning |
|---|---|
--pair IP --pin PIN --name "…" | one-time pairing through a node |
--dir DIR | where blobs are kept (default /var/lib/sensmos-store) |
--capacity 100G | space you offer; M, G, T suffixes |
--install-service | copy to /usr/local/bin, write and enable the systemd unit |
--be URL | alternative backend, for testing |
--direct-port 9033 | inbound port answering with the agent's banner |
--no-direct | do not listen on any inbound port |
--no-upnp | do not ask the router to forward that port (it is asked by default, and renewed while the agent runs) |
The three settings above are written into /etc/sensmos-store.json and the program exits — the service runs with no arguments, so pass them once and restart it.
The unit it writes:
# /etc/systemd/system/sensmos-store.service
[Unit]
Description=Sensmos store agent
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/bin/python3 /usr/local/bin/sensmos-store.py
Restart=always
RestartSec=10
User=root
[Install]
WantedBy=multi-user.target
Pairing — the token
The agent has no credentials of its own. It asks your node (with the node's PIN, from inside the LAN) to vouch for it; the node relays the request to the backend, and the backend issues a token scoped to store.serve — serving Store sessions, nothing else. The token is stored in /etc/sensmos-store.json (mode 0600) and the agent renews it by itself. One node can vouch for up to four attachments. The full contract is on Attachments & the EXT API.
Updates
The agent never updates itself: it runs as root in your LAN, and a server that could push code to it would be a way into your network. To update, repeat step 1 and restart:
sudo curl -fsSL -o /usr/local/bin/sensmos-store.py https://sensmos.com/ext/sensmos-store.py
sudo systemctl restart sensmos-store
From store-py/0.4 on, files travel directly and what you receive is verified. An older script still sells, proves and earns, but every byte it handles passes through the backend and it accepts a broken file without noticing. The app shows a note on the seller tile when a newer version exists.
From store-py/0.5 on, an interrupted transfer resumes instead of starting over. If a link drops mid-file, the part you already received stays on disk and the next attempt carries on from there — the same in both directions, and the same for copies rebuilt between hosts. You only ever offer back the part you have verified, so a resumed file is checked exactly as strictly as one that arrived in a single go.
Updating takes nothing away from you. The files you already hold, the format on disk, the daily proof and your earnings are untouched — 0.5 reads and answers for them exactly as 0.4 did. An unfinished piece is scratch space, never stored data: it goes the moment the owner calls the upload off, or after a week if nobody comes back to it, and either way you are neither paid nor charged for it.
An outdated agent stops receiving new files. From store-py/0.2 on, the agent writes the .meta record described above; a script that cannot write it is left out when sellers are picked, so it keeps serving and proving what it already holds but is given nothing new. The app says so on the seller tile, and the log says it at every connect.
Early days
Store is new and the pool is small. If you set up a seller and nothing arrives for a while, that is the honest state of demand, not a fault on your side — say hello on Discord and we will tell you what the pool looks like.