Documentation

Relay developer docs

Everything you need to encode a frame on a device, move it through Relay, and verify it on ingest.

Quickstart

Install the SDK, create a channel key, and you can encode and verify frames in a few lines. Relay never sees your key — masking and verification happen entirely in your own runtime.

# install
npm install @cobalt/relay

# or pin a release
npm install @cobalt/relay@2.4.0

Encode and verify

The SDK exposes two pure functions, encode and decode. Both take the channel key; neither performs any network I/O.

import { encode, decode } from "@cobalt/relay";

const KEY = "cobalt";                       // per-channel key

const frame  = encode("sensor reading", KEY);
const result = decode(frame.hex, KEY);

console.log(result.payload);              // "sensor reading"
console.log(result.verified);             // true

Frame format

A Relay frame is compact and fixed in shape. The payload is masked; the checksum is computed over the unmasked payload so a receiver can confirm a clean decode.

FieldWidthNotes
version1 byteformat version, currently 0x02
channel1 bytelogical channel id
payloadn bytesmasked with the repeating-key transform
crc2 bytesFletcher-16 over the unmasked payload

Encoding reference

The masking transform is a repeating-key XOR with a position-bound key offset. For a payload of length n and a key of length m, byte i is masked with the key byte at index i mod m:

// mask (encode)        — i runs 0 .. n-1
out[i] = payload[i] ^ key[i % key.length];

// unmask (decode)      — XOR is its own inverse
payload[i] = frame[i] ^ key[i % key.length];

Because XOR is self-inverse, encode and decode are the same operation — the only thing that has to match is the key index. The first payload byte pairs with key[0], the second with key[1], and the index wraps with % key.length. Shift that index by even one slot and every byte downstream resolves to the wrong value.

Common mistake

The most frequent integration bug we see is an off-by-one in the key index — pairing payload byte i with key[i+1] instead of key[i]. The decode runs without errors but produces corrupted bytes, and the checksum fails. If your output looks like noise, check your slot alignment first.

Checksums

Relay uses Fletcher-16 over the unmasked payload. It's cheap on constrained hardware and catches the byte-shift and transposition errors that plague long-running radio links. The receiver recomputes it after decode and compares against the two trailing frame bytes.

function fletcher16(bytes) {
  let s1 = 0, s2 = 0;
  for (const b of bytes) {
    s1 = (s1 + b) % 255;
    s2 = (s2 + s1) % 255;
  }
  return (s2 << 8) | s1;
}

A matching checksum means the frame decoded cleanly. A mismatch means either the frame was corrupted in transit or the decoder unmasked it out of alignment — both are treated as a dropped frame.

Troubleshooting

Decode returns corrupted bytes

If a frame decodes without throwing but the payload is unreadable and the checksum fails, the transform is misaligned. Verify that you're masking each byte against key[i % key.length] — not key[(i+1) % key.length] or any other offset. The decode is position-sensitive; alignment is the usual culprit.

Checksum always fails

Confirm you're computing Fletcher-16 over the unmasked payload, not the raw frame, and that you parse the incoming hex with radix 16. A wrong radix silently produces the wrong bytes.

Output is shorter than expected

Make sure your byte loop steps by two characters per hex byte (i += 2) and reads to the full length of the frame.

Still stuck?

Reproduce against the live sandbox — it runs the exact decoder shipped in the SDK against a known-good sample frame, so you can compare behavior byte for byte.