Multiplayer for
Bitcoin inscriptions.
Lumamesh connects browsers directly to each other — no server in the data path. The only primitive that works inside an inscription sandbox.
The inscription sandbox problem
Bitcoin inscriptions run in a strict content sandbox. fetch() is blocked. WebSocket is blocked. Importing scripts from external URLs is blocked. The only network primitive that still works is WebRTC DataChannels — and that requires a signaling step to exchange SDP offers between browsers.
Lumamesh provides that signaling layer via a static WebRTC connection to a relay node. The relay config is embedded in the page — no external network call is needed to bootstrap it. Once two browsers have exchanged SDP, the relay drops out completely.
🎮 Multiplayer games
Real-time game state sync between players. 60 Hz unreliable mode for positions, reliable mode for events.
💬 Encrypted chat
Room key derived from the room name. No server ever sees plaintext.
🗳 On-chain voting
Inscriptions that coordinate in real time without a backend.
🌐 Any web page
Works in regular pages and browser extensions too — not just inscriptions.
Add multiplayer in 4 steps
Import the library
From a Bitcoin inscription — import by inscription ID, served from Bitcoin, no external fetch:
// In your inscription's <script type="module">
import { connect } from '/r/content/LUMAMESH_INSCRIPTION_ID';
From a regular web page:
import { connect } from 'https://lumamesh.com/lumamesh.js';
Connect to the network
const net = await connect();
This opens a WebRTC DataChannel to the nearest relay. Relay config is fetched from Bitcoin (sat 534764996818532) then cached in localStorage — subsequent loads connect instantly.
Join a room
const room = await net.joinRoom('my-game-v1', { nick: 'alice' });
All browsers that call joinRoom with the same name connect to each other. The room name is hashed to derive the AES-GCM encryption key — it never leaves the browser in plaintext.
Private room (only people with the link can connect):
const room = await net.joinRoom('my-game-v1', {
nick: 'alice',
secret: location.hash.slice(1) // #abc123 — never sent anywhere
});
Send and receive
// Broadcast to everyone in the room
room.broadcast({ x, y, angle });
// 60 Hz position updates — lower latency, no retransmit
room.broadcast({ x, y, angle }, { reliable: false });
// When peers connect
room.on('peer', peer => {
peer.on('open', () => console.log(peer.nick, 'joined'));
peer.on('data', msg => handleState(peer, msg));
peer.on('close', () => removePlayer(peer.id));
});
Complete inscription boilerplate
<!DOCTYPE html>
<html>
<body>
<!-- your game UI here -->
<script type="module">
import { connect } from '/r/content/LUMAMESH_INSCRIPTION_ID';
const net = await connect();
const room = await net.joinRoom('my-game-v1', { nick: 'player1' });
room.on('peer', peer => {
peer.on('open', () => addPlayer(peer.id, peer.nick));
peer.on('data', msg => updateState(peer.id, msg));
peer.on('close', () => removePlayer(peer.id));
});
// Send your game state every frame
function tick() {
room.broadcast({ x: player.x, y: player.y }, { reliable: false });
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
</script>
</body>
</html>
LUMAMESH_INSCRIPTION_ID with the inscription ID of lumamesh.js — contact the operator or check the GitHub repo for the current ID.Inscribing your game
Test locally first
// Swap the import to the HTTPS version while testing
import { connect } from 'https://lumamesh.com/lumamesh.js';
Open in two browser tabs, check peers connect.
Switch import to inscription ID
import { connect } from '/r/content/LUMAMESH_INSCRIPTION_ID';
Inscribe with ord
ord wallet inscribe --file game.html --fee-rate 5
The inscription ID from the output is your game's permanent address on Bitcoin.
Two layers, no servers
Layer 1 — Relay network. A WebRTC DataChannel connection (UDP, no HTTP) to a community relay node. The relay only exchanges SDP offers between browsers — it never sees room content.
Layer 2 — Direct mesh. Once browsers exchange SDP over Layer 1, a direct RTCPeerConnection opens between them. The relay drops out of the path. All data is end-to-end AES-GCM encrypted with a key the relay never has.
Relay config. The relay IP, DTLS fingerprint, and node identity are stored as a re-inscribable blob on sat 534764996818532. Adding a new relay node means re-inscribing the blob — every game picks it up automatically with no code change.