G

Wire Format & Protocol Reference

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).