⬡ Lumamesh

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.

WebRTC DataChannels AES-GCM encrypted No WebSocket No fetch() Permanent on Bitcoin

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

1

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';
2

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.

3

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
});
4

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>
Replace 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

1

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.

2

Switch import to inscription ID

import { connect } from '/r/content/LUMAMESH_INSCRIPTION_ID';
3

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.

Run your own relay node →