Wire Format & Protocol Reference
Technical reference for developers implementing Meshii Protocol clients or building relay node software.
Envelope Format (Binary)#
All messages on Gao Network use this binary envelope format. Little-endian byte order.
Offset Size Field ──────────────────────────────────────────────────────── 0 4 Magic bytes: 0x4D534849 ("MSHI") 4 1 Version: 0x01 5 1 Flags (bitmask): bit 0: has_priority bit 1: is_group_message bit 2–7: reserved 6 2 Reserved: 0x0000 8 32 Envelope ID (UUID v4, binary) 40 32 Routing Tag (recipient identifier, opaque) 72 8 TTL expires (Unix timestamp, seconds) 80 8 Created at (Unix timestamp, milliseconds) 88 4 Ciphertext length (N bytes) 92 N Ciphertext (Signal-encrypted payload) 92+N 64 Sender HMAC (relay auth — not sender identity) ──────────────────────────────────────────────────────── Total minimum: 156 bytes + ciphertext
Decrypted Payload (MessagePack)#
After Signal decryption, the inner payload is MessagePack encoded:
{
"v": 1,
"t": "text",
"c": "Hello Alice!",
"ts": 1700000000000,
"r": "envelope-id-being-replied-to",
"m": {}
}
Field
Type
Description
v
int
Payload version
t
string
Message type: text, image, call, notification, agent_action
c
string
Content (UTF-8 for text)
ts
int
Sender timestamp (ms)
r
string?
Reply-to envelope ID (optional)
m
object?
Type-specific metadata (optional)
Relay API#
Node operators implement these endpoints. The relay is intentionally minimal — a dumb encrypted message bus.
Enqueue
POST /relay/enqueue
Auth: Meshii capability token
Body: {
routing_tag: string, // opaque recipient identifier
ciphertext: base64, // Signal-encrypted envelope
ttl_seconds: number // max 604800 (7 days)
}
Response: { envelope_id: string }
Fetch
GET /relay/fetch?since=<unix_timestamp>
Auth: Meshii capability token (recipient proves queue ownership)
Response: {
envelopes: [
{ envelope_id, ciphertext, created_at }
]
}
Acknowledge
POST /relay/ack
Auth: Meshii capability token
Body: { envelope_ids: string[] }
Response: { deleted: number }
Critical: Relay MUST delete envelopes synchronously before returning 200. The delete must complete before the response is sent.
Relay Auth Model#
Relay uses short-lived capability tokens — not persistent API keys.
Token = Sign(
recipient_routing_tag, // which queue this token accesses
action: "read" | "write",
expires_at: now + 5min, // short TTL
nonce, // unique per request
identity_key // Ed25519 private key (client-side)
)
Writer token (sender): proves sender is allowed to write to recipient’s queue (via Contact Token or mutual contact gate).
Reader token (recipient): proves ownership of queue by signing a server-issued challenge with their Identity Key.
Relay verifies signature against public key. Does not store identity mappings. Tokens expire in 5 minutes — cannot be replayed.
WebRTC Signaling Protocol#
The signaling server exchanges SDP and ICE candidates. It never sees message content.
Register
{ "type": "register", "routing_tag": "...", "token": "jwt" }
Offer / Answer / ICE
{ "type": "offer", "to": "routing_tag", "sdp": "..." }
{ "type": "answer", "to": "routing_tag", "sdp": "..." }
{ "type": "ice", "to": "routing_tag", "candidate": "..." }
Call Signaling
{ "type": "call-request", "to": "routing_tag", "call_type": "audio" }
{ "type": "call-accept", "to": "routing_tag" }
{ "type": "call-reject", "to": "routing_tag" }
{ "type": "call-end", "to": "routing_tag" }
Abuse protection: Signaling server enforces:
-
Auth gate: caller must have Contact Token or mutual contact before SDP is forwarded
-
Rate limit: max 10 call initiations per sender per hour
-
ICE flood protection: max 50 ICE candidates per session
-
Unauthenticated SDP offers dropped silently (no error response — prevents probing)
Key Exchange (X3DH)#
When Alice wants to send Bob her first message, she needs Bob’s pre-key bundle from the key server.
Publish Pre-Key Bundle
POST /keys/publish
Auth: JWT token
Body: {
identity_key: string, // base64 Ed25519 public key
signed_pre_key: { key_id, public_key, signature },
one_time_pre_keys: [{ key_id, public_key }, ...] // batch of 100
}
Fetch Pre-Key Bundle
GET /keys/{routing_tag}
Response: {
identity_key: string,
signed_pre_key: { key_id, public_key, signature },
one_time_pre_key?: { key_id, public_key } // consumed — removed from server
}
One-time pre-keys are consumed one by one. If exhausted, the signed pre-key is returned instead (slightly reduced forward secrecy for that session).
Delete-on-Delivery Sequence#
1. Sender enqueues envelope → relay stores
2. Recipient comes online → GET /relay/fetch
3. Recipient decrypts message
4. Recipient sends POST /relay/ack { envelope_ids }
5. Relay deletes envelope BEFORE returning 200
6. Relay returns { deleted: 1 }
If ACK never arrives → relay purges after TTL (7 days).
Node Registry Contract (Base L2)#
Node operators register on-chain to join the network.
interface IMeshiiNodeRegistry {
function registerNode(
address operator,
string calldata endpoint, // "wss://relay.example.com"
string calldata region, // "us-east" | "eu-west" | "ap-southeast"
uint256 stakeAmount // in $GAO (wei)
) external;
function getActiveNodes()
external view returns (NodeInfo[] memory);
function slashNode(
address nodeOperator,
SlashReason reason,
bytes calldata evidence
) external;
function initiateUnstake() external;
function finalizeUnstake() external; // after 7-day cooldown
}
Contract deployed on: Base mainnet (address published at meshii.app/contracts)
Versioning#
Version
Changes
v0.1
Initial protocol — WebRTC + Relay + Signal crypto
v0.2 (planned)
Group message optimization, wire format v2
v1.0 (planned)
MLS for 100+ member groups, full permissionless network
Protocol upgrades go through governance (28-day notice for breaking changes).