Lapis Net is a fully decentralized peer-to-peer social network protocol on the Kotlin/JVM platform. It is part of the Lapis family of distributed systems, alongside Lapis Cloud.
The protocol combines:
-
Decentralized identity — secp256k1 keypairs, Bitcoin-compatible
-
Content-addressed storage — Nabu (IPFS-on-libp2p), DHT + Bitswap
-
Web-of-trust reputation — Veritas, a shortest-path trust graph
-
Gossip-based propagation — libp2p GossipSub
-
Bitcoin/Lightning-anchored micropayment relevance — Virtus (on-chain OP_RETURN and Lightning BOLT-11 payment proofs)
-
Sybil-resistant free "likes" — Karma, weighted by Veritas and a Bitcoin time anchor
-
Machine-to-machine infrastructure reputation — Madli, a five-metric weighted-median node-reputation layer
The protocol core is deliberately neutral about curation, sorting, and reputation models. Veritas, Karma, Virtus, and Madli (the scoring dimensions) are optional tools for browser/view implementations, not protocol requirements.
Every wave through V0.9 is implemented and merged to master, followed by four
post-arc maintenance waves (V0.9.8 to V0.9.14, below), a fix release and a feature release. The current release is
v0.11.0 (v0.10.0 was the first with a chronological version number: up to v0.9.9 the tags
are wave identifiers (V0.7 was never a standalone wave — its scope was folded into the
incremental browser growth of V0.2–V0.5), from v0.10.0 on they count releases in the
order they ship). See docs/roadmap.adoc for the
explanation and a map of every tag to its content. Full technical
detail for each module is in the Modules table below; the full wave-by-wave
history — including scope cuts and hardening follow-ups — is in
docs/roadmap.adoc and
docs/architecture.adoc.
| Wave | What it added | Status |
|---|---|---|
V0.1 |
Protocol core: secp256k1/Ed25519 identity, libp2p bootstrap, Nabu (DHT + Bitswap) storage, Veritas (web-of-trust) |
✅ |
V0.2 |
Virtus (LTR micropayment economy, on-chain |
✅ |
V0.3 |
Karma (Sybil-resistant free "likes", Bitcoin time-anchored) |
✅ |
V0.4 |
Genesis-bootstrap, QR-code/deep-link identity linking, keystore encryption at rest (Argon2id + AES-256-GCM) |
✅ |
V0.5 |
Madli (machine-to-machine node reputation, weighted-median aggregation) |
✅ |
V0.6 |
Lightning payment-proof verification ( |
✅ |
V0.7 |
(no standalone wave — folded into V0.2–V0.5’s incremental browser growth) |
— |
V0.8 |
Direct messages (X3DH/Double Ratchet, online + offline delivery, encrypted attachments, acceptance policy, browser UI) and 1:1 voice calls over WebRTC |
✅ |
V0.9 |
Asynchronous messages / mail replacement: encryption, threading, attachments, spam protection |
✅ |
V0.9.8 |
Repair of cross-node DHT provider discovery at the storage layer (broken since V0.1.4) |
✅ |
V0.9.9 |
NAT traversal over Circuit Relay v2: a node behind NAT publishes a relay address and can be dialled through the relay (opt-in relay operation, no hole punching) |
✅ |
V0.9.10 |
Relay hardening: a stalled circuit can no longer block the other circuits and reservation renewals sharing its connection (per-circuit stall timeout plus a per-connection stall budget), and a node reuses a relayed connection the peer already opened to it (released in v0.10.0) |
✅ |
V0.9.11 |
DHT layer hardening in |
✅ |
v0.10.1 |
Fix release: a race in the relay’s circuit setup (present in |
✅ |
v0.11.0 (V0.9.12) |
Madli relay-reliability metric (daily vector wire format v2, v1 stays readable; the publisher is off by default), usage-based relay priority (reputation is never a hard gate), automatic relay selection (two relays per node, only for a node declared |
✅ |
v0.11.0 (V0.9.13, V0.9.14) |
DHT fallback next to GossipSub for peer records, prekey bundles, mail and direct-message pointers (IPNS value per identity, own IPNS responder and validator, bounded record store, outbox, rendezvous provider records, consumers). Plaintext mail leaves no DHT pointer. Wired into the browser node and |
✅ |
v0.11.0 (V0.8.7b) |
|
✅ |
Known, deliberate gaps — not oversights, each documented at the point it was cut:
-
No embedded Lightning node — proof verification only, no payment sending; no BOLT-12 offer publication.
-
NAT traversal is relay-only — no hole punching. A node behind NAT can now be reached: it reserves a slot on a Circuit-Relay-v2 relay and publishes the resulting
/p2p-circuitaddress, and a peer that holds only that address dials it through the relay with an end-to-end Noise session the relay cannot read. Acting as a relay is opt-in and off by default. What is still missing isdcutrhole punching, which would let two NAT’d peers upgrade a relayed connection to a direct one: it is absent fromjvm-libp2pentirely and would have to be implemented from scratch. Every relayed connection therefore stays relayed for its whole life, and relay operators are not compensated: reputation (Madli) is the intended route: a relay-reliability metric exists sincev0.11.0, but the publisher that writes it is off by default and no application switches it on, and the usage-based priority ledger is local and memory-only (a new identity starts at 0). Using a relay is an explicit, programmatic setting (LapisNode.relayClient.reserve(relay)). Sincev0.11.0the library also has an automatic relay selection (LapisNode.startAutoRelay, only for a node declaredNodeReachability.PRIVATE) and a reachability heuristic that merely warns; there is still no AutoNAT, and the browser andlapisnet node --reachability=privatestart it (the publisher stays off unless--publish-relay-reliability=true), andlapis-net-callrefuses relayed media on purpose, so voice calls still need a direct connection. Rollout constraint: a record advertising the newRELAYcapability bit is rejected outright by older nodes, so readers must be upgraded everywhere before any node advertises it. -
Browser call remote control (V0.8.7b) —
/call.htmlremote-controls a call whose audio runs in the JVM node process. Since V0.9.15 the acceptance policy is wired in the browser: blocked callers are shown locally and nothing is sent to them. No mute, no persisted call history (the history is held in memory). -
No group calls, video, multi-device messaging, or MLS — cut from the V0.8 arc’s scope entirely on 2026-08-04, not a partial or stubbed implementation.
-
No delivery receipts or typing indicators for direct messages — deliberately deferred, not built even as stubs.
-
The DHT fallback is wired since V0.9.15 (V0.9.13, V0.9.14). The browser publishes and looks up peer records, prekey bundles and mail/DM pointers over the DHT (GossipSub stays the primary path); it is on by default and
--dht-fallback=falseturns it off. The CLI’snodesubcommand uses it for the peer directory only. DHT lookups tell the peers that are asked who is looking for whom, until traffic anonymisation exists (docs/architecture.adoc, section 11); the rendezvous key of a victim can be eclipsed by throwaway identities; plaintext mail is deliberately never recoverable from the DHT; a lookup needs the owner’s Ed25519 peer ID.
How Lapis Net stands next to the five closest decentralized social protocols — full methodology and sources at net.lapisproject.dev/comparison.
| Feature | Lapis Net | Nostr | Secure Scuttlebutt | Farcaster | Bluesky / AT Proto | Mastodon / Fediverse |
|---|---|---|---|---|---|---|
Genuine peer-to-peer (no required relay, hub, or homeserver) |
● |
◐ |
● |
○ |
○ |
○ |
Cryptographic identity instead of an account |
● |
● |
● |
◐ |
◐ |
○ |
Weighted web of trust (transitive trust paths) |
● |
◐ |
◐ |
○ |
○ |
○ |
Bitcoin/Lightning-native value layer |
● |
● |
○ |
○ |
○ |
○ |
Market-based visibility (real value at stake, not a black-box algorithm) |
● |
○ |
○ |
◐ |
○ |
○ |
End-to-end encrypted voice calls |
● |
○ |
○ |
○ |
○ |
○ |
Asynchronous long-lived messaging (e-mail replacement) |
● |
◐ |
◐ |
○ |
○ |
○ |
Machine-to-machine node reputation (infrastructure, not opinions) |
● |
○ |
○ |
○ |
○ |
○ |
● yes · ◐ partial · ○ no
Honest caveat: Nostr, Mastodon, Bluesky and Farcaster each already have millions of users and mature client ecosystems. Lapis Net is, as of this comparison, a working multi-wave prototype (V0.1–V0.9 implemented) without a public network yet — a real trade-off against the table above, not a detail to gloss over. One clarification on the first row: Lapis Net can use volunteer relays to reach nodes behind home routers; they forward only end-to-end-encrypted traffic and are never required for a node that is directly reachable.
| Module | Purpose |
|---|---|
|
Base protocol abstractions shared by all other modules. |
|
secp256k1 identity keypairs, Ed25519 dual-key binding for the libp2p peer ID, keystore encryption at rest (Argon2id + AES-256-GCM), and multi-device/pseudonym self-trust linking. |
|
DHT + Bitswap content storage via Nabu. Cross-node provider discovery works since V0.9.8 (it had been broken since V0.1.4); the DHT layer is bounded and validates untrusted input since V0.9.11. |
|
Veritas (web-of-trust): trust-edge data model, shortest-path scoring algorithm, GossipSub propagation, Nabu persistence. |
|
libp2p node bootstrap, peer identity derivation, mDNS discovery, GossipSub, the Genesis-bootstrap/QR-code connection flow, and relay-based NAT traversal over Circuit Relay v2 (V0.9.9: reservation client, opt-in relay server, |
|
Virtus (the LTR micropayment economy): on-chain (OP_RETURN) and Lightning (BOLT-11) payment proofs, 10%/24h decay, view-specific accumulation. |
|
Karma (free likes): Bitcoin time-anchored, Veritas-weighted Sybil-resistant "like" scoring. |
|
Madli (machine-to-machine node reputation): five-metric observation vector (plus, since V0.9.12, an optional relay-reliability block in wire format v2, written only if the off-by-default publisher is enabled) with Veritas-weighted, manipulation-resistant weighted-median aggregation. |
|
Asynchronous messages (the e-mail replacement), fully implemented (V0.9.1–V0.9.4): a signed, addressed |
|
Acceptance-gate machinery extracted from |
|
Signed peer/presence directory records ( |
|
X25519 encryption sub-key and the X3DH handshake (Signal’s Extended Triple Diffie-Hellman, reimplemented from the public specification): |
|
The custom |
|
1:1 audio calls over WebRTC (V0.8.7): |
|
Minimal-Browser — the first real Lapis Net client, exposing Veritas/Virtus/Karma data, the LTR record-authoring endpoints, a mail inbox UI (V0.9.3), and a direct-message UI (V0.8.6b), and the call remote control |
|
Entry point / multi-node demo harness proving trust propagation end to end. |
# headless node behind a NAT: reserves relays, uses the DHT directory (no DM, no mail)
lapisnet node --reachability=private
# browser node: same switches as arguments, LAPISNET_* variables or $LAPISNET_HOME/browser.properties
lapisnet-browser --reachability=private --dm-veritas-gate=true
# mail gates (unreleased, V0.9.17): off by default; mail from a sender who passes none of them is discarded
lapisnet-browser --mail-veritas-gate=true --mail-karma-min-score=2Mail acceptance is wired in the browser node (unreleased, after v0.11.0). Keys: --mail-veritas-gate (LAPISNET_MAIL_VERITAS_GATE, file key mailAcceptance.veritasGate), --mail-karma-min-score (LAPISNET_MAIL_KARMA_MIN_SCORE, mailAcceptance.karmaMinScore), --mail-min-deposit-msat (LAPISNET_MAIL_MIN_DEPOSIT_MSAT, mailAcceptance.minDepositMsat, no effect yet). All gates are off by default. Accepted contacts are always let through; blocked mail is discarded without reply and only counted in the status panel.
An invalid option aborts the start. See docs/release-notes/v0.11.0.adoc for all options, defaults and the metadata warning about the DHT fallback.
./gradlew clean check # compiles, runs tests, runs ktlint
./gradlew :lapis-net-cli:runRequires JDK 25.
To run the reference node together with its minimal browser UI (main view, mail inbox, direct messages):
./gradlew :lapis-net-browser:runIt prints the local address to open in a browser and the identity fingerprint. For calls it also prints a link of the form http://127.0.0.1:7878/call.html#t=<token>; (same device only: the token is regenerated on every start and must not be shared or logged; open the link from the console, a bare /call.html is refused). The HTTP port is
currently fixed at 7878 (there is no environment variable for it yet), so only one browser node can
run per machine; a second instance on the same machine fails with "address already in use" and would
also share the same identity and storage directory unless LAPISNET_HOME is changed. State lives under
$LAPISNET_HOME (default ~/.lapisnet). Environment variables: LAPISNET_KEYSTORE_PASSPHRASE encrypts
the identity at rest (without it, and without an interactive console, the identity is stored
unencrypted with POSIX file permissions only and a warning is logged); LAPISNET_LISTEN_ADDR sets the
libp2p listen address (the default is loopback-only, so no other machine can reach the node until this
is set); LAPISNET_BOOTSTRAP_PEERS is a comma-separated list of peer multiaddrs, each including
/p2p/<peerId>, and entries in $LAPISNET_HOME/bootstrap.peers (one per line) are added to it.
There is no public network yet, and connecting two real nodes is still a manual step. Set
LAPISNET_LISTEN_ADDR to the machine’s concrete LAN address, for example /ip4/192.168.1.20/tcp/4001.
The main page shows a connect link (lapisnet://connect?maddr=…&pk=…) and the same link as a QR
code; on the other node, paste that link into the "Paste a connect link" field of its main page, or
enter the plain multiaddr, including /p2p/<peerId>, in the connect field in the page header. The
peer ID is also logged at startup (BrowserServer listening on … (peer <peerId>)). A wildcard such
as /ip4/0.0.0.0/tcp/4001 makes the node listen on every interface, but the QR code and connect link
would then carry the wildcard, which another machine cannot dial; in that case type
/ip4/<LAN-IP>/tcp/4001/p2p/<peerId> into the header connect field by hand, taking the peer ID from
the log. Peers discovered through mDNS are only logged and
cached, never dialled automatically and not shown in the UI. If you only have one machine,
./gradlew :lapis-net-cli:run runs a multi-node trust-propagation demo inside a single process.
Releases are listed at https://github.com/lapisproject-dev/Lapis-Net/releases. v0.10.1 is the first release built by the release pipeline, whose publish step was tested only against local stand-ins before that run, not against GitHub itself. Starting with
v0.10.1 (the current release is v0.11.0), a release is published together with lapis-net-browser-<version>.zip,
lapis-net-cli-<version>.zip and SHA256SUMS. Releases before that, including v0.10.0, have no
downloadable files: check the release’s asset list, and build from source (see Building) if none
are attached.
Prerequisite: JDK 25 on the PATH or in JAVA_HOME.
unzip lapis-net-browser-<version>.zip
lapis-net-browser-<version>/bin/lapis-net-browser # node plus minimal browser UI, see Building
unzip lapis-net-cli-<version>.zip
lapis-net-cli-<version>/bin/lapis-net-cli # multi-node demo, ends by itself after a few secondsThe ZIPs also contain .bat start scripts for Windows; those are not tested. The environment
variables described under Building, in particular LAPISNET_HOME and
LAPISNET_KEYSTORE_PASSPHRASE, apply to the unpacked distributions as well.
Verify a download against SHA256SUMS, which lists both ZIPs, so the check has to be limited to the
file you actually downloaded (<file> is its name, for example lapis-net-cli-<version>.zip):
# Linux
sha256sum -c --ignore-missing SHA256SUMS
# macOS (shasum has no --ignore-missing, so select the line of your file)
awk '$2 == "<file>"' SHA256SUMS | shasum -a 256 -c -The artifacts are not signed; the checksums only detect corrupted downloads. How releases are built and published: docs/releasing.adoc.
-
Code style is enforced by ktlint and checked in CI.
-
Logging uses kotlin-logging:
private val logger = KotlinLogging.logger {}in every file that logs. -
Documentation is written in AsciiDoc; diagrams (once introduced) use kUML via the
kumlAsciidoctor macro — no PlantUML, Mermaid, or hand-drawn diagrams. -
All identifiers (classes, interfaces, packages) are named in English. Domain terms (Veritas, Karma, Virtus, Madli) are the one exception, as they are proper nouns from other languages used directly as identifiers: Veritas and Virtus are Latin, Karma is Sanskrit, and Madli is Georgian.
See docs/architecture.adoc and docs/roadmap.adoc for more.
Apache License 2.0 — see LICENSE.
See CONTRIBUTING.adoc.
