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.
| Field | Width | Notes |
|---|---|---|
| version | 1 byte | format version, currently 0x02 |
| channel | 1 byte | logical channel id |
| payload | n bytes | masked with the repeating-key transform |
| crc | 2 bytes | Fletcher-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.
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.
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.