XPRS

eXtended Packet Radio System — protocol specification

XPRS carries position, movement, weather, telemetry and messages between stations over licence-free spectrum, amateur bands and the internet. It extends and modernizes APRS. A packet is at most 250 bytes of key:value text, readable on sight, signed by the key that generated the sender's callsign, and the same on every bearer, LoRa, Bluetooth, WiFi, HF/VHF/UHF or the internet. No licence is needed on licence-free spectrum; on amateur bands it operates under amateur rules, plain text, a government-issued callsign, no ciphertext (sections 9.4 and 33).

Part I. Foundations

The rules every field obeys, where a callsign comes from, what a packet looks like on the wire, how it is named, and how it is signed.

1. Purpose

XPRS is APRS with Store and Forward: provides an alternative for both licensed and unlicensed radio frequencies. The design is built with three decades of experience from APRS with features for the modern times. APRS proved that a small text packet, heard by everyone and repeated by volunteers, is enough to build a live map of who is where, doing what, needing what. XPRS keeps that idea whole and rebuilds the parts that three decades of use showed to be limits:

The prerequisites changed too. APRS requires amateur spectrum and an issued callsign, correct for a licensed service, and excluding everyone without a licence. XPRS runs the same design on Bluetooth and LoRa in the ISM bands, on WiFi and on the internet, with identity derived from a keypair generated on the device; licensed operators keep their callsigns and their bands, and the two meet under APRS rules where they meet (sections 7.4 and 32).

The format itself follows the most expensive lesson APRS taught. APRS accumulated its encodings one field at a time: four incompatible position formats, weather as fixed-width digits inside a position report, telemetry whose units arrive in separate messages, and a mixture of feet, knots, miles per hour, Fahrenheit, hundredths of an inch and tenths of a millibar. Each addition was constrained by a packet that was already full. XPRS is one syntax, readable on sight, with room to grow (section 2).

1.1 The actors

The same words recur through this document, and each names a ROLE, one station is usually several of them at once, and every one of them is somebody's ordinary device rather than appointed infrastructure.

Actor What it is Where
operator the person; holds the key their stations sign with, answers for what they transmit 3, 9
station one device speaking XPRS under a callsign; an operator's phone, tracker and desktop are stations sharing a base callsign 3.1
relay (APRS: digipeater) repeats a packet on the medium it heard it, within the hop budget, appending itself to via: 9.1-13.2
carrier holds a message in custody for a station that is absent, and hands it over, with a signed receipt, when it can 9.3-13.7
gateway (APRS: iGate) republishes packets onto something that is not XPRS, an archiver, APRS-IS, the internet, under the scope rules, and publishes who it hears so the absent stay reachable 9.11.3, 12.1, 12.8
file server holds content-addressed files and serves them by hash (serve:files, cmd:file, q:have) 7.7, 24
archiver keeps packets and answers for them: the spool of what it heard (re-aired on cmd:history), the publications of its depositors, and mail held for recipients (t:mailbox hold: names one deliberately). Every station is one at its own scale; serve:archive announces the role to others 24, 11.2, 36
group an address several stations read, a name, not a boundary, and not a station 7.3, 26

The table is descriptive, not a licence scheme: a phone in a pocket is a station, a relay when it repeats, a carrier when it holds, a gateway the moment it has both a radio and the internet. What a station DOES for others it advertises with serve: (section 13), and what it is obliged to do is nothing (section 30.3), every role here is volunteered and bounded by its operator's budgets.


2. Design rules

  1. Every packet is a list of key:value fields separated by one space. There are no positional fields, no binary framing and no escaping.
  2. Every packet declares its type in the first field, so a station never has to guess what it is holding.
  3. Every key has a declared value type (section 4.3). A reader knows the shape of a value before reading it, and a key's meaning never varies with the packet it appears in.
  4. Any field may appear anywhere and any field may be absent. Adding a field never changes how an existing field is read.
  5. One packet type carries every kind of observation. New data means a new key, never a new packet type.
  6. Nothing is defined out of band. No receiver requires prior state to read a packet.
  7. Every measurement carries its unit, so no value on the wire is ambiguous and no receiver has to assume one.
  8. An unknown key is skipped and an unknown packet type is ignored, in both cases without error.
  9. A station asks for what it wants in q: and answers with the same words in s:. There is one vocabulary, not one per direction.
  10. Values are text. Compression is not used.

A packet is readable without a decoder:

t:message f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 m:net starts in ten minutes

78 bytes. A person reads it, and so does a five-line parser.


3. Callsigns

An XPRS callsign is X1, X2, X3, X4 or X5 followed by two to five characters derived from the holder's public key:

X1 = person or operator
X2 = movable station: a ship, an aircraft, a bus, a car
X3 = fixed station, relay or unattended equipment
X4 = automated device, operated by a controller (section 11.7.1)
X5 = group (section 10)

A group holds a keypair like anything else, so it gets a callsign like anything else. What differs is only who holds the private half: a person for X1, a machine for X2 and X3, the controller that operates the device for X4, and whoever administers the group for X5.

X2 and X3 split on one question: is the station going anywhere. An X2 moves, a ship, an aircraft, a bus, a car, so its position is a reading that expires and is worth asking for again. An X3 is where it was: a relay on a roof, a buoy on a mooring, a repeater on a hill. A reader may cache an X3's whereabouts and plan around them; an X2's whereabouts are traffic. The choice is made at key generation and is a statement of intent, not a promise the network checks, a fixed station trucked to a new site keeps its callsign and announces from where it now stands, and site: (section 14) still says per announcement whether a station stays put. Everything a station does in this document, relaying, carrying, archiving, being owned and commanded (section 11.9), an X2 does as an X3 does; the prefix states what its position means, not what it may do.

Those characters are taken from the bech32 encoding of the key, so the letters b, i and o and the digit 1 never appear in them.

How many characters is the holder's own choice, and four is the default. A station that never chooses shows four, which is what every self-generated callsign in the field is. The choice is made once, when the key is generated, and is then fixed: the callsign is how the holder is addressed and stored, and a station that changed it would be a different station to everyone who had heard it.

Callsigns are always uppercase and are not a fixed length. A callsign issued by a radio authority is equally valid on the wire, including a suffix:

t:message f:CT1ABC-9 d:G0XYZ/P ts:2026-08-08_14:26:40 m:gate is closed, use the east path

89 bytes. Nothing in this format assumes a callsign length.

An XPRS callsign is a label, not an identity, and a shorter one is a weaker label:

characters callsigns two holders collide after forging one costs
2 1,024 ~40 holders ~1,024 keypairs
3 32,768 ~226 holders ~32,768 keypairs
4 1,048,576 ~1,283 holders ~1,048,576 keypairs
5 33,554,432 ~7,259 holders ~33,554,432 keypairs

Collisions can be produced deliberately at any of these lengths, at two characters, in about a thousand tries. A receiver that needs to establish identity verifies a signature against the full public key (section 6). No authority issues, revokes or vouches for an X1, X2, X3, X4 or X5 callsign.

That last sentence has a consequence on the air: a self-generated callsign may never be transmitted on licensed spectrum, where identifying the station is a condition of the licence. Section 6.4.1 states the rule and section 6.4.2 shows how an operator ties a callsign that was issued to them to the key they sign with.

3.0.1 A callsign is matched whole, never by prefix

Because the characters are a prefix of the key's encoding, one key can derive four different callsigns: a holder whose key encodes to qpzr8... could show X1QP, X1QPZ, X1QPZR or X1QPZR8. They are four different labels, and the holder wears exactly one of them.

The order of operations is fixed, and inverting it would break section 3.1:

strip the device suffix first, then compare the bare callsign as a whole string

X1ABCD-1  and  X1ABCD-2   the same person on two devices
X1AB      and  X1ABCD     different labels, even from one key

A station answers to its own bare callsign and to its own suffixed forms, and to no other truncation of its key. A receiver never resolves a callsign with a prefix match.

Checking that a callsign could have come from a key, recomputing the bech32 encoding and comparing that many characters, does not establish which length the holder chose, since all four truncations of one key pass such a test. What makes a callsign canonical is the identity announcement that declares it (section 15.6), which is signed.

3.1 One person, several devices

The same person runs a phone, a tablet, and a node in the shed. All three hold the same key and show it at the same length, so all three derive the same callsign, and on the air they are one callsign saying three different things.

APRS answered this with an SSID: CT1ABC-9. XPRS keeps that notation, because it is the one every operator already reads, and changes what sits underneath it.

The bare callsign is the person. A suffix is one of their devices.

X1A67X      the person, on whatever device is in reach
X1A67X-1    one of their devices
X1A67X-2    another

Numbers run -1 to -99. -0 is never written: the bare form already means the person. The suffix costs two or three bytes on f: and on d:, and nothing anywhere else.

3.1.1 Choosing a number, without anyone to ask

A device starts bare and stays bare while it is alone, most stations never number themselves at all.

It learns it is not alone the moment it hears a beacon (section 15.6) carrying its own callsign and a different key. Two devices of one person share the person's key; they do not share the key their own destination is derived from, and on a beacon that difference is visible as lx::

t:observation f:X1A67X-1 link:ble peers:2 lx:5463d9bc93aebda57d1f704a3cfbee80
t:observation f:X1A67X-2 link:ble peers:2 lx:23698e7593f05e2053f5183580e2cf98

77 bytes each. Each device then takes the lowest number it has not heard in use, and when two would take the same one, the device whose own key sorts lower keeps it and the other picks again. Both sides compute that from what they have already heard, so there is no negotiation, no registry, no coordinator, and no state that can be lost, only a rule that two devices apply to the same facts and reach the same answer.

A number, once adopted, is kept across restarts. A device that has been alone for a long time may drop back to bare, and nothing breaks if it does not.

3.1.2 What the numbers mean

APRS gave its SSIDs conventional meanings, and a generation of operators reads them fluently: -9 is the car, -7 is the handheld, -13 is a weather station. XPRS keeps those meanings for -1 to -15, unchanged, so an operator who has been reading them for twenty years reads ours correctly and a gateway maps both ways without a table of its own.

Suffix APRS convention XPRS type: it usually goes with
(bare) primary / home station , the person
-1 to -4 generic, digipeater digi, node
-5 other networks, smartphone portable
-6 special activity, satellite, camping portable
-7 handheld portable
-8 boats, RV, second mobile boat, sailboat
-9 primary mobile car
-10 internet gateway node
-11 balloon, aircraft, spacecraft balloon, airplane, glider, drone
-12 trackers, DTMF, RFID devices drone, node
-13 weather station wx
-14 trucker, full-time driver truck
-15 generic, digipeater digi

-16 to -99 mean nothing at all, and that is the extension. APRS stopped at 15 because four bits was what it had. We have room, so the numbers above 15 are plain serials for a person with more devices than the conventions have names, a fourth tablet is not a kind of thing, it is the fourth one.

Where the two disagree, type: wins. A suffix is a hint for people and for gateways; type: (section 35) is the machine-readable answer, and APRS never had one:

t:observation f:X1A67X-9 type:car link:ble peers:2 lx:5463d9bc93aebda57d1f704a3cfbee80

86 bytes. A receiver never infers what a station is from its suffix when type: is present, and never rejects a station for wearing a number that disagrees with it. The conventions are there to be helpful, not to be enforced.

This also improves the numbering above: a device that knows its own type: prefers the conventional number for it when that number is free. A phone in a car self-numbers to -9 without anyone typing anything, and an operator who has never read this document still guesses right.

3.1.3 What is addressed to whom

d: Reaches Use it for
X1A67X the person, on whatever device is reachable everything ordinary
X1A67X-2 that one device what only that machine can answer

Ordinary traffic names the person, because a person is not their tablet:

t:message f:X1RD89 d:X1A67X ts:2026-08-12_17:28:52 m:on my way

62 bytes. Every device of that person shows it, and the sender does not have to know or care which screen its reader is in front of.

A suffix is for when the machine is the point, the photos are on the tablet, the sensor is wired to the shed:

t:message f:X1RD89 d:X1A67X-2 ts:2026-08-12_17:28:52 m:the tablet has the photos

80 bytes.

Commands are the other way round. cmd:door-open addressed to a person is meaningless, a person is not a door, so a command (section 11) names a device:

t:command f:X1A67X-1 d:X1A67X-2 ts:2026-08-12_17:28:52 cmd:status

65 bytes. A station receiving a command addressed to the bare callsign may refuse it, and should, unless it is the only device wearing that callsign.

A station accepts a packet addressed to its own suffixed name or to the bare callsign, and no other. Everything a receiver keys on a person, a thread, a reputation, a block, a follow, keys on the bare callsign, so numbering a device never splits a conversation in two.

3.1.4 Two devices, one recipient

Both will answer, and that is correct rather than a fault to suppress. A receipt (section 9.7.1) names the device that sent it:

t:receipt f:X1A67X-1 d:X1RD89 r:40f357 s:ack sig:<60 characters>
t:receipt f:X1A67X-2 d:X1RD89 r:40f357 s:ack sig:<60 characters>

109 bytes each, both naming the one message in r:. The sender collapses them on that identifier: one message, delivered, and, if it cares to look, delivered to two of the three devices that person carries.

Mail held for a bare callsign (section 6.3) is discharged by handing it to any device wearing it. The person has their message; the carrier's job is finished. Getting that message onto their other devices is not the mesh's business, it is the same key on all of them, and their own software can settle it between themselves.

3.1.5 What a suffix is not

It is not authentication, and nothing should be built as though it were. Every device of one person holds the same private key, so any of them can sign as that person and any of them can claim any number. A suffix distinguishes devices that are cooperating, never devices that are competing.

The device's real name on the wire is lx:, a destination derived from a key that device alone holds. When it genuinely matters which machine is being addressed, that is what proves it; the digits after the hyphen are a convenience for people.

Which is the difference from APRS worth stating plainly. There the SSID was typed by the operator and meant something by convention. Here the person is proven by the key, the device is proven by lx:, and the suffix exists so a human can say which one they mean.

3.2 Callsigns of other networks

A gateway (section 9.11.3) carries people who are not on XPRS at all: the users of a Meshtastic mesh on the same LoRa channel, today, and of other networks later. They have no key XPRS knows and no callsign anyone issued, so the gateway writes their own address in a form that can never be mistaken for either:

MT + the node number in eight uppercase hexadecimal digits   a Meshtastic node
MC + the first four bytes of its public key, the same way      a MeshCore node
120  t:message f:MTA1B2C3D4 ts:2026-09-19_12:04:00 scope:regional zmid:a1b2c3d44683c668 via:X3DCK0 m:anyone on the XPRS side?
73  t:message f:X1QZ3N d:MTA1B2C3D4 ts:2026-09-19_12:05:10 m:yes, on the quay

The first is a Meshtastic user's message on the public channel, as the gateway X3DCK0 wrote it, scope:regional because that channel is one valley's air (section 9.11.5); the second an XPRS user's answer, direct, which any gateway in range carries back. Four rules:

The letters are the network's, so a further one takes two more letters and nothing else changes. X stays XPRS's alone.


4. Packet

key:value key:value key:value ...

Sixteen characters lets a key be a word a reader already knows, produces:, or that word with a prefix, lifeproduces:, and keeps keys short enough that they never crowd the values out of a packet. Earlier drafts allowed eight. A station built to that limit skips a longer key the way it skips any key it does not know (design rule 8), so nothing it could already read changes.

The maximum packet is 250 bytes on every transport. This fits one LoRa packet, one BLE5 extended advertisement, and the store-and-forward buffer of the smallest station. Content that does not fit is split into parts (section 7.6), never compressed.

On LoRa the frame depends on the station's LoRa mode (section 14.8). In xprs mode the packet is the frame. In meshtastic mode each packet travels inside a Meshtastic frame of its own: a 16-byte header and a few bytes of wrapper leave 233 bytes, so a packet of 234 to 250 bytes goes as two frames and is joined again before anybody reads it. The limit above is unchanged; the framing is LoRa's business and no other bearer sees it.

m: is the one field whose value may contain spaces, which is why it is last: everything after m: is the message. It needs no delimiter and no escaping, so a message may contain spaces, colons, URLs and any punctuation.

4.1 Envelope keys

Key Type Meaning
t enum packet type, always the first field
f call from: the sending callsign
d call destination: a callsign, a group name, or absent for a broadcast
ts time when the packet was composed, UTC
tz offset the sender's offset from UTC, for display
q words what the sender wants back (section 8)
s words what this packet answers or reports (section 8)
r hex6 the identifier of another packet this one refers to
n ratio this packet is part i of n
tag labels topic labels chosen by the sender (section 4.5)
cw words what the packet contains, warned before rendering (section 4.6)
urg enum how much this is worth carrying (section 9.5)
scope scope how far this may be relayed, default global (section 9.11)
lang lang language of m:, default English (section 4.7)
hold path preferred mailboxes, in order (section 9.12)
serve words what a station does for others (section 13)
cmd label the action a command asks for (section 11)
owner path who may command a station: its allow-list (section 11.9)
use enum who may originate traffic through a station (section 11.9)
first path senders whose packets a station airs ahead of others (section 11.9)
ssid text the WiFi network a station is to join, sealed (section 11.10)
pass text its password, only ever sealed (section 11.10)
wifi enum a station's WiFi: join, off; on a result up, joining, failed, off (section 11.10)
ip text the IPv4 address a station holds on its network, dotted, on a result (section 11.10)
zone enum a station's time zone: auto, or an offset (section 11.10)
ap enum a station's own access point, on or off (section 11.10)
lora enum a station's LoRa mode: xprs, meshtastic or meshcore (sections 11.10, 14.8)
key enum new: a station is to make a new key (section 11.10)
nsec bech32 a private key a station is to take, only ever sealed (section 11.10)
arg words its arguments
code int what happened, on a result
near qty how close to dest counts as arrived (section 6.4)
relay path callsigns the sender asks to relay this packet, in order (section 9.2.2)
route path the route a receipt is acknowledging (section 9.10)
add enum something this packet adds (section 7.5)
remove enum something this packet withdraws (section 7.5)
grant path callsigns admitted to a group (section 10)
revoke path callsigns removed or suspended (section 10)
role enum what a grant confers: mod, sub, or absent for a member
hide enum what a moderator withdraws from view: message
mood enum how the sender feels (section 16.1)
only call narrows a replay to one callsign or group (section 11.2)
opt labels the choices in a poll, two to six (section 17)
root hex6 the packet a thread hangs from (section 7.4)
size qty how large a file is (section 7.7.1)
vote label the option chosen in a poll (section 17.3)
via path callsigns that relayed this packet, oldest first (section 9)
track label name of a track this packet belongs to (section 21)
title label name of a post or event, stable across revisions
dest coord where a passage is bound (section 19)
onboard int how many people are aboard
price money what is being asked or offered (section 16.1)
freq qty a frequency (section 14)
bw qty bandwidth
shift qty repeater input, as an offset from freq
input qty repeater input frequency, stated outright
tone qty access tone
power qty transmit power
mode enum how a channel is modulated
ch label channel number in a band plan
range qty expected usable range, an estimate
site enum whether the station stays where it is
source enum what powers the station, or where produces: comes from (sections 14.3, 15.5.2)
every qty how long between recurring windows
for qty how long each window lasts
at clock time of day a cycle is anchored to, UTC
seq int position of this point within that track
kind enum nature of an event, values per packet type (sections 15, 16); in cmd:history the packet type asked for, or a comma-separated list of them (section 11.2)
sev enum severity of a warning (section 24)
rad qty radius of the area affected or asked about (sections 16, 17, 28)
since time when the condition started, or will start
until time when the sender expects the condition to end
m text human-readable content, always last
file ref content hash and type of a referenced file
name label filename, when the extension is not enough (section 7.7.1)
ph ref content hash of a file's piece list (section 7.7.2)
count int on t:file kind:folder, how many files a listing holds (7.7.3); on an archiver's serve:archive announcement, how many RECORDS it holds, never how many callsigns (13.0.1)
b b64 a small file's bytes, inline (section 7.7.4)
ih label BitTorrent infohash, 40 hexadecimal characters (section 7.7.5)
have b64 what a station holds of a file: a chunk or piece bitfield, least significant bit first; or, the two stated exceptions, the word full and a fraction such as 412/900 (section 8.1)
off qty byte offset a cmd:file transfer resumes from (section 11.2)
x b64 sealed body
xr b64 hidden parts of a redacted packet (section 6.2.1)
sig base85 signature
k bech32 public key, in t:identity and t:challenge, and on q:owner, a claim and a new key's answer (sections 11.9, 11.10)

4.2 Packet types

t: Purpose
message a message, to a station, a group, or anyone in range
observation an observation: position, movement, weather, telemetry
receipt a receipt or an answer to a request
reaction a reaction to another message
request a request for data another station holds
identity an identity announcement, binding callsign to public key
track a point in a named track (section 21)
sos a call for help (section 23)
info a notice about conditions (section 25)
blog a published post (section 18)
poll a question put to everybody, with the choices (section 17)
file what a file is, so it can be wanted (section 7.7.1)
report a claim that a packet is spam or abuse (section 20)
place somewhere useful that is not the sender (section 22)
status a short post about the sender, now (section 16)
passage where a vessel is going (section 19)
event something happening at a time and place (section 26)
offer what a station has (section 27)
need what a station wants (section 27)
channel a frequency a station uses (section 14)
mailbox stations that hold mail for the sender (section 9.12)
service what a station does for others (section 13)
command asks a station to do something (section 11)
result what happened to a command
moderate an act of authority in a group (section 10)
challenge a challenge to prove a callsign (section 29)
response the answer to a challenge
warning a warning about a hazard (section 24)
ping a reachability test
pong a reply to ping

An unknown type is ignored. It is never an error and is never displayed as a message. Types not listed here are reserved (section 26).

4.3 Value types

The type is fixed by this document and is never transmitted.

Type Form Example
int digits, optional leading - 210
dec digits, optional leading -, optional single . and fraction -9.1393
enum one lowercase word from a list given with the key foot
words one or more lowercase words separated by commas ack,read
label lowercase letters, digits and -, at least one character field-day
labels one or more label, separated by commas field-day,photos
call uppercase letters, digits, - and /; a group name has the same form CT1ABC-9
path one or more call, separated by commas X32DVA,CT1ABC-9
hex6 exactly 6 lowercase hexadecimal characters 399227
time YYYY-MM-DD_HH:MM:SS, UTC 2026-08-08_14:26:40
offset +HH:MM or -HH:MM +05:45
coord two dec separated by a comma, latitude then longitude 38.7223,-9.1393
ratio two digits 1 to 9 separated by /, position then total 2/3
epoch two int separated by a dot, boot counter then seconds 7.4210
scope local, regional, global, or ISO 3166-1 alpha-2 codes separated by commas PT,ES
lang an ISO 639-1 code, optionally / and a region PT/BR
nick 1 to 16 ASCII letters, digits, - and _ joao-brito
clock HH:MM:SS, a time of day in UTC 20:00:00
money an amount with an ISO 4217 code, optional leading ~ and / period, or one of offers, swap, free (section 16.2) ~25EUR/day
qty a number followed immediately by its unit (section 15.9) 48km/h
ref 43 base64url characters (a SHA-256, no padding), a dot, 1 to 8 lowercase alphanumerics nyxKz...L4Q.jpg
b64 base64url, no padding pQ4m9xT2vB8kR
bech32 a bech32 string npub1qz3n7...
base85 60 characters, base85, no space
text any bytes, spaces included heading south on the N8

A value that does not match its declared type is skipped, as an unknown key is. A packet is never rejected as a whole because one field is malformed. One field is read more carefully than skipped: a scope: word a receiver does not know stops it relaying the packet (section 9.11), because a skipped scope would read as global. One character is removed before the type is checked: the block character █ marks a redacted portion (section 6.2.1), and a reader strips it and reads what remains.

4.4 Numbers

The decimal separator is a dot. A comma is never part of a number.

c:14.2        14.2 degrees
c:-3.5        3.5 below zero
a:11240       eleven thousand two hundred and forty metres
rh:0.4        four tenths of a millimetre

This is not a preference between conventions. A comma is already structural in this format: it separates latitude from longitude in pos:38.7223,-9.1393 and it separates words in q:ack,read. A station writing temp:14,2 for 14.2, or alt:11,240 for eleven thousand, produces a packet that reads as two values.

Trailing zeros are significant, because the number of decimal places states the precision claimed (section 15.1). temp:14.0 says the reading was measured to a tenth; temp:14 says it was not.

Software that formats numbers according to the operator's locale must be overridden before transmission. This is the most likely way for an implementation to emit packets that every other station silently misreads, since 14,2 is correct in most of Europe and is a different packet here.

4.5 Labels

tag: carries topic labels chosen by the sender. They let a receiver file, filter or search a packet without reading it.

t:message f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 tag:vacation m:back on Monday, radio off until then

102 bytes. Several labels are separated by commas:

t:message f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 tag:vacation,photos m:the coast near Sagres

94 bytes. There is no limit on how many labels a packet carries beyond the 250-byte packet itself. It applies to any packet, not only messages:

t:observation f:X3WX01 pos:38.7223,-9.1393 temp:14.2C ts:2026-08-08_14:26:40 tag:field-day

90 bytes.

A label contains no space. No field value may contain a space, m: alone excepted, and tag: is not that exception. A label is lowercase letters, digits and -, at least one character, and words are joined with - rather than spaces: tag:field-day, never tag:field day, which would end the field at the space and leave day to be read as a malformed field and skipped.

Labels are lowercase so that a receiver matching them does not have to decide whether Vacation and vacation are the same label. They are chosen freely and this document assigns none: unlike q: and s:, whose words are a fixed vocabulary because both ends must agree on what they mean, a label means whatever the people using it agree it means.

A receiver that does not recognise a label keeps it and displays it. Labels are never a routing decision: d: says where a packet goes, and a label never changes that.

4.6 Content warnings

cw: warns a receiver what a packet contains before it renders it.

t:blog f:X1QZ3N ts:2026-08-08_14:26:40 title:haulout cw:injury m:the hand is fine now, photos below

99 bytes. cw: costs nine bytes and takes one or more words, separated by commas:

Word Contents
adult sexual content
nudity nudity that is not sexual
violence violence
injury graphic injury, blood, surgery
death death, human or animal
drugs drug or alcohol use
language profanity
spoiler spoils something the reader may not have seen
flashing rapid flashing or strobing
other something else the sender thinks needs a warning

flashing is not a matter of taste. Rapid flashing triggers seizures in photosensitive epilepsy, and a receiver that autoplays is the case the warning exists for:

t:info f:X3RLY7 pos:38.7223,-9.1393 kind:event cw:flashing ts:2026-08-08_14:26:40 m:fireworks over the harbour at ten

117 bytes.

It is a closed vocabulary rather than a tag:. A label means whatever the people using it agree it means (section 4.5), which is fine for a topic and useless for a filter: a receiver cannot hide adult content reliably if the word for it is whatever each sender chose, in whatever language. These ten words a receiver can act on, and translate.

It is several words rather than one rating, because a single scale cannot say why. Somebody avoiding graphic injury is not the same person as somebody avoiding sexual content, and neither is the reader who needs flashing.

Four rules, each covering a way this otherwise fails without anyone noticing.

It covers the whole packet, including any file:. The attachment is usually the thing that needed warning about:

t:message f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 cw:adult,nudity file:nYxKzGm4vT2pQ8dW5jR7cL0aFbNs9hUe3oXiC6EkM1w.jpg m:not for the group chat

144 bytes.

It is repeated on every part of a split message, not only the first. Parts arrive in any order (section 7.6), so a warning carried once is a warning the receiver may read after it has already displayed part two.

It stays in cleartext when the body is sealed. cw: is an envelope field and is never moved inside x:, because a receiver has to decide whether to render before it decrypts:

t:message f:X1QZ3N d:X1RD89 ts:2026-08-08_14:26:40 cw:adult x:<64 characters> sig:<60 characters>

191 bytes. What the packet contains is disclosed; the content itself is not.

A relay never strips it, and its absence is not a guarantee. An unmarked packet is unmarked, not safe. A receiver that treats a missing cw: as a promise has built a filter on the honesty of strangers.

cw: marks content. It does not regulate it: what is lawful to send or offer differs by country, and that is the operator's business and not the format's.

t:offer f:X1QZ3N pos:38.6902,-9.4012 kind:other cw:adult price:~40EUR/h ts:2026-08-08_14:26:40 m:massage, by appointment

120 bytes.

4.7 Language

lang: says what language the text is in. It is optional and the default is English, so a packet without it is read as English.

t:message f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 lang:PT m:a rede comeca daqui a dez minutos

94 bytes, nine of them the field. An ISO 639-1 code in uppercase.

A regional variant is added after a slash, which is worth the three bytes where it changes the words rather than the accent:

t:message f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 lang:PT/BR m:a rede comeca daqui a dez minutos

97 bytes. PT/BR and PT are the same language and not the same vocabulary, and EN/US and EN/GB disagree about enough words to matter in a warning.

Uppercase and the slash both follow conventions already in the format: values are uppercase where they are codes rather than words, and / already separates in n:2/3 and in a callsign like G0XYZ/P.

lang: describes m: and nothing else. A packet with no text does not need it, and the keys, the packet types and every enum value in this document are English regardless, they are identifiers rather than prose, and translating them would break every receiver.

A receiver that cannot read the language still relays it. Translation is a presentation matter, and a station that dropped what it could not read would make the network useless to anybody in a minority language.

4.8 Time

ts: is written the way a person reads it, in UTC:

ts:2026-08-08_14:26:40

The _ keeps it one field.

ts: is always UTC, so two packets from opposite sides of the world are ordered by comparing them directly, with nothing to convert first.

tz: optionally carries the sender's offset from UTC, so a reader can show the local time it was written at:

t:message f:VK2XYZ d:X1QZ3N ts:2026-08-08_14:26:40 tz:+11:00 m:good morning from Sydney

87 bytes. The receiver reads 14:26 UTC, and knows it was 01:26 the next morning where the sender was standing. Offsets of 30 and 45 minutes exist, so the minutes are written out rather than assumed to be zero.

tz: is presentation only. It never changes ts:, never takes part in an identifier, and a station that ignores it loses nothing but the courtesy.

A packet that may be relayed or carried must have a time field. A carried packet can be delivered days later, and an undated position is plotted as current. Two alternatives exist for stations without a clock (section 15.7).

4.9 Extending the format

A new field takes an unused key, declares its type, and is placed anywhere. Receivers that do not know the key skip it and its value. No existing field moves, no packet type is added, and no version is negotiated.

Keys beginning with z are reserved for private and experimental use and are never assigned by this document.

t:observation f:X3WX01 pos:38.7223,-9.1393 temp:14.2C zpm:8 ts:2026-08-08_14:26:40

82 bytes. Every existing receiver reads temp:14.2 and ts:, skips zpm:8, and is otherwise unaffected.

4.10 JSON

A packet is a flat list of key:value pairs, so it is a JSON object already. The conversion needs no schema, no table and no knowledge of any packet type.

t:message f:X1QZ3N d:X1RD89 ts:2026-08-18_09:15:00 m:arrived at the marina
{"t":"message","f":"X1QZ3N","d":"X1RD89","ts":"2026-08-18_09:15:00",
 "m":"arrived at the marina"}

Wire to JSON. Read fields left to right. Each is a key, a :, and a value. Split on the FIRST colon only, a value may contain colons, and m: usually does. Fields are separated by exactly one space, except that everything after m: is the message, spaces included. A key is 1 to 16 lowercase letters and digits and appears once (section 14.4), so it is a valid and unique JSON member name with nothing to escape.

Every value is a JSON string. temp:14.2C is "21.2C" and not 14.2: the unit is part of the value (section 4.4), and a reader that converts to a number has silently thrown away what the number meant. peers:2 is "2". There are no JSON numbers, no booleans and no nulls anywhere in a packet, a value is never empty (section 4), so a key that carries no information is simply absent.

JSON to wire. Write t: first, m: last where present, one space between fields, no space after a colon. The result is a packet, subject to the 250-byte limit like any other.

The one thing to be careful with is order. A JSON object is unordered by definition, and the identifier in section 5 is a SHA-256 over the wire bytes. Converting a packet to JSON and back can therefore reorder the middle fields and produce a DIFFERENT identifier for the same message. Two consequences, and neither is a limitation of JSON so much as a rule about where the identifier lives:

For reading, filtering, indexing and shipping packets between programs, the object form is enough and the order does not matter. It matters when a signature or an identifier has to still be true afterwards.


5. Message identifiers

Every packet has an identifier, and it is never transmitted. Both ends compute it from the packet itself, byte for byte:

  1. Take the packet as transmitted: the UTF-8 bytes on the wire.
  2. Remove the sig: field and the via: field where present. Removal means deleting the key, its value, and the ONE space before the key, leaving every other byte untouched. Both fields sit before m: (which is last and swallows everything after it), fields are separated by single spaces, and t: is always first, so the removal is a plain byte deletion and the result reads as if neither field had been written.
  3. Compute SHA-256 over the remaining bytes.
  4. The identifier is the first 6 characters of the digest in lowercase hex.

To reply to a packet, compute its identifier this way and put it in r:. Verifiable at a shell:

$ echo -n 't:message f:X1QZ3N ts:2026-08-08_14:26:40 m:OK' | sha256sum
ca5413...        ->  id ca5413

Nothing announces its own identifier. A packet already carries who sent it and when, so the identifier is free.

The timestamp is what makes this work. Hashing content alone would give every OK ever sent the same identifier, and OK is the most common message on any network:

t:message f:X1QZ3N ts:2026-08-08_14:26:40 m:OK   ->  ca5413
t:message f:X1QZ3N ts:2026-08-08_14:27:22 m:OK   ->  08ba5f
t:message f:X1RD89 ts:2026-08-08_14:26:40 m:OK   ->  25f96d

Sender, second and text together are unique in practice.

A split message is the one place to be careful: every PART has its own identifier, and the message's identifier is computed from the reassembled packet as section 7.6 describes, n: removed, the m: values joined. A reply names the reassembled identifier, never a part's.

Two fields are excluded and both for the same reason: they change while a packet is in flight. Signing must not alter the identifier of what was signed, and a carrier appending itself to via: must not turn one message into a different one. Everything else is included, which is what makes the identifier exact.

f: and ts: alone would not be enough. A station beaconing position and weather in the same second would give both packets one identifier, and a reply naming it would be ambiguous. Hashing the whole packet costs nothing, because none of it goes on the wire.

r: carries an identifier when a packet refers to another: a reply, a reaction, a receipt, or a withdrawal of the sender's own earlier packet (section 11.2).


6. Signing and privacy

6.1 Signatures

sig: covers the whole packet with the sig: and via: fields and their separating spaces removed. Position in the packet is therefore not significant, and a verifier reconstructs the signed text by deletion.

Both fields come out, and the reason for via: is not economy. A relay appends itself to via: (section 9), so a signature covering it would break at the first hop and every relayed packet would read as forged, on a network whose whole point is relaying, and where signing is the default. The signed text is therefore exactly the text the identifier is derived from (section 5), which leaves one canonical form to implement rather than two.

t:message f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 sig:<60 characters> m:net starts in ten minutes

143 bytes. The identifier is 399227, the same as the unsigned packet in section 7.3, because signing changes neither f:, ts: nor the payload.

State Condition
verified signature present, valid, signer key known
forged signature present and invalid
unverified signature present, signer key unknown
unsigned no signature

sig: may appear on any packet, and a station signs by default.

A callsign is a label that anyone can write (section 3). Nothing else in this format stops a station putting f:X1QZ3N on a packet it did not send, and for most traffic a signature is the only thing that does. It is 65 bytes and it should be spent unless there is a reason not to.

Default means default and not mandatory. A sender may omit it, and a receiver must accept an unsigned packet rather than discarding it, because the network carries traffic from sensors with no key, from stations too small to sign, and from software written before this section. What a receiver must not do is present an unsigned packet as though its f: were established.

One exception remains, and it is not economy.

A challenge and its response carry their own proof: the response is signed, and the exchange is the authentication rather than something needing it (section 29).

Everything else is signed by default, including the smallest packets:

t:reaction f:X32DVA d:LISBOA r:399227 add:like sig:<60 characters>

111 bytes for a signed reaction, against 46 unsigned. A forged reaction attributed to you is still an impersonation, and the extra bytes buy the same thing they buy anywhere else.

6.1.1 When the signature does not fit

A signature is 65 bytes and the packet limit is 250, so a full observation can run out of room. The weather station in section 33.3 is 193 bytes and would be 258 signed.

Drop optional fields, never the signature.

t:observation f:X3WX01 pos:38.7223,-9.1393 temp:14.2C hum:78% press:1013.2hPa wind:3.4m/s wdir:210deg type:wx ts:2026-08-08_14:26:40 sig:<60 characters>

197 bytes: the same station with two readings fewer, signed. A receiver learns less and can trust what it learns, which is the better trade for a station whose whole value is being believed.

Where the payload cannot be trimmed, split it (section 7.6) and sign the last part, which is what a long message already does. Where neither is possible, a single measurement that fills the packet by itself, the sender may go unsigned, and that is the case the "unsigned" state above exists to describe.

6.1.2 Computing the signature

The scheme is a short Schnorr signature over secp256k1 in the classic (e, s) form: the challenge travels truncated to 16 bytes and the scalar in full, 48 bytes together, the smallest a secp256k1 signature can be. It uses the same key that stands behind the station's callsign (section 3), and it is NOT interoperable with BIP-340 verifiers, the truncated-challenge form trades that away for 16 bytes a packet gets back.

Two helpers, then the algorithm.

Tagged hash (the BIP-340 construction): H_tag(msg) = sha256(sha256(tag) || sha256(tag) || msg), with the tag as UTF-8 text.

Base85: 4 bytes become 5 characters, Z85-style. The value v of each big-endian 4-byte group is written as five digits base 85, most significant first, using this 85-character alphabet (chosen to exclude the space and the APRS-reserved {, |, ~):

0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-+=^!/*?&<>()[]%$#@,;_

48 bytes therefore encode to exactly 60 characters, which is what the sig: value is.

To sign, with private scalar d, curve order n and generator G:

  1. m = sha256(canonical text), the packet with sig: and via: removed, exactly as section 5 removes them. m is also the digest the identifier truncates, so one canonical form serves both.
  2. Use the x-only key convention: if d*G has an odd y coordinate, replace d with n - d. Let px be the 32-byte x coordinate of the public point.
  3. Draw 32 random bytes aux, and derive the nonce k = H_XPRS/nonce(d || m || aux) mod n (d as 32 bytes big-endian; if the result is zero, use one).
  4. R = k*G; let rx be its 32-byte x coordinate.
  5. e = first 16 bytes of H_XPRS/challenge(rx || px || m).
  6. s = (k + e*d) mod n, with e read as a big-endian integer.
  7. The signature is e || s, 48 bytes; base85-encode it and place it in sig:, which with-inserts before m: so the message stays last.

To verify, split the 60 characters back into e (16 bytes) and s (32): reject s >= n; lift px to the even-y point P; compute R' = s*G - e*P; recompute step 5 over the x coordinate of R'; the signature is valid when the 16 bytes match. The verifier never needs the signer's y coordinate or the nonce, s*G - e*P reconstructs R because s = k + e*d.

Because aux is random, signing the same packet twice produces two different signatures and both verify. A worked example with aux FIXED to 32 zero bytes so every value is reproducible, using the toy key d = 7 (never sign with a toy key):

canonical  t:message f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 m:net starts in ten minutes
m          39922745225b987201d0a253ed152b99712088ba6c578a41bdfc670594a3c553
px         5cbdf0646e5db4eaa398f365f2ea7a0e3d419b7e0330e39ce92bddedcac4f9bc
k          e052dd3b72c2aab12db5d39d047de17c82fef6268b1b07e0dd157e826ba4aa5b
rx         f3552a7235ef03791e8469f3bf55f041f21a9afcd65675301bdc11d8f95d9ea8
e          b40348c6defc8e1ae6dfca7635513e3b
s          e052dd3b72c2aab12db5d39d047de1816f15f396a402ea9d2d3407bde0dd5df8
sig (48B)  b40348c6defc8e1ae6dfca7635513e3be052dd3b72c2aab12db5d39d047de1816f15f396a402ea9d2d3407bde0dd5df8

Which yields the signed packet, note m above is the digest whose first six characters are the identifier 399227 of section 7.3:

143  t:message f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 sig:V<-(s&U-xL(hjs8hbML0<8nw[A)a<YeW+5_1BYlWzX.)fQYP&LeI[ZC<n4Yl m:net starts in ten minutes

The signing function in C++, with the curve and hash primitives taken from the reader's own library (libsecp256k1, OpenSSL or equivalent, the shapes are stated in the comments):

// XPRS short-Schnorr sign over secp256k1 (section 6.1.2).
// Assumed primitives:
//   void sha256(uint8_t out[32], const uint8_t* data, size_t len);
//   bignum arithmetic mod n (curve order) and EC ops:
//     Point ec_mul_g(const Big& k);          // k * G
//     Big   point_x(const Point& p);         // x coordinate
//     bool  point_y_odd(const Point& p);
//     Big   big_from_be(const uint8_t* b, size_t len);
//     void  big_to_be32(const Big& v, uint8_t out[32]);

static void tagged_hash(uint8_t out[32], const char* tag,
                        const uint8_t* msg, size_t len) {
    uint8_t th[32];
    sha256(th, (const uint8_t*)tag, strlen(tag));
    std::vector<uint8_t> buf;
    buf.insert(buf.end(), th, th + 32);
    buf.insert(buf.end(), th, th + 32);
    buf.insert(buf.end(), msg, msg + len);
    sha256(out, buf.data(), buf.size());
}

// canonical: the packet text with sig: and via: removed (XPRS section 5).
// d: private scalar. aux: 32 fresh random bytes. out_sig: 60 chars + NUL.
void xprs_sign(const std::string& canonical, Big d,
               const uint8_t aux[32], char out_sig[61]) {
    static const char* B85 =
        "0123456789abcdefghijklmnopqrstuvwxyz"
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ.-+=^!/*?&<>()[]%$#@,;_";
    const Big n = curve_order();

    uint8_t m[32];
    sha256(m, (const uint8_t*)canonical.data(), canonical.size());

    Point P = ec_mul_g(d);                        // x-only key convention
    if (point_y_odd(P)) { d = n - d; P = ec_mul_g(d); }
    uint8_t px[32];  big_to_be32(point_x(P), px);

    uint8_t nb[96];                               // d || m || aux
    big_to_be32(d, nb); memcpy(nb + 32, m, 32); memcpy(nb + 64, aux, 32);
    uint8_t kh[32];  tagged_hash(kh, "XPRS/nonce", nb, 96);
    Big k = big_from_be(kh, 32) % n;
    if (k == 0) k = 1;

    uint8_t rx[32];  big_to_be32(point_x(ec_mul_g(k)), rx);

    uint8_t cb[96];                               // rx || px || m
    memcpy(cb, rx, 32); memcpy(cb + 32, px, 32); memcpy(cb + 64, m, 32);
    uint8_t e16[32]; tagged_hash(e16, "XPRS/challenge", cb, 96);

    Big e = big_from_be(e16, 16);                 // first 16 bytes only
    Big s = (k + e * d) % n;

    uint8_t sig[48];                              // e(16) || s(32)
    memcpy(sig, e16, 16); big_to_be32(s, sig + 16);

    for (int i = 0; i < 48; i += 4) {             // base85, 4 bytes -> 5 chars
        uint32_t v = (uint32_t(sig[i]) << 24) | (uint32_t(sig[i+1]) << 16) |
                     (uint32_t(sig[i+2]) << 8) | uint32_t(sig[i+3]);
        for (int j = 4; j >= 0; --j) { out_sig[(i/4)*5 + j] = B85[v % 85]; v /= 85; }
    }
    out_sig[60] = 0;
}

And verification, with two more primitives from the same library, ec_add(a, b) for point addition and lift_x(x) returning the even-y curve point for a 32-byte x coordinate (null when x is not on the curve):

// Verify a sig: value (XPRS 9.1.2). canonical: the packet text with sig:
// and via: removed. sig85: the 60-character value. px: the signer's 32-byte
// x-only public key. Returns true only for a valid signature.
bool xprs_verify(const std::string& canonical, const char sig85[60],
                 const uint8_t px[32]) {
    static const char* B85 =
        "0123456789abcdefghijklmnopqrstuvwxyz"
        "ABCDEFGHIJKLMNOPQRSTUVWXYZ.-+=^!/*?&<>()[]%$#@,;_";
    const Big n = curve_order();

    uint8_t sig[48];                              // base85 decode, 5 -> 4
    for (int i = 0; i < 60; i += 5) {
        uint64_t v = 0;
        for (int j = 0; j < 5; ++j) {
            const char* d = strchr(B85, sig85[i + j]);
            if (!d) return false;                 // not in the alphabet
            v = v * 85 + (d - B85);
        }
        if (v > 0xFFFFFFFFull) return false;      // overfull group
        sig[(i/5)*4 + 0] = (v >> 24) & 0xff;
        sig[(i/5)*4 + 1] = (v >> 16) & 0xff;
        sig[(i/5)*4 + 2] = (v >>  8) & 0xff;
        sig[(i/5)*4 + 3] =  v        & 0xff;
    }

    Big e = big_from_be(sig, 16);                 // e(16) || s(32)
    Big s = big_from_be(sig + 16, 32);
    if (s >= n) return false;

    uint8_t m[32];
    sha256(m, (const uint8_t*)canonical.data(), canonical.size());

    Point P = lift_x(px);                         // even-y point for px
    if (P.is_null()) return false;

    // R' = s*G - e*P, computed as s*G + (n - e)*P.
    Point Rp = ec_add(ec_mul_g(s), ec_mul(P, n - e));
    if (Rp.is_infinity()) return false;
    uint8_t rx[32];  big_to_be32(point_x(Rp), rx);

    uint8_t cb[96];                               // rx || px || m
    memcpy(cb, rx, 32); memcpy(cb + 32, px, 32); memcpy(cb + 64, m, 32);
    uint8_t e2[32];  tagged_hash(e2, "XPRS/challenge", cb, 96);

    uint8_t diff = 0;                             // constant-time compare
    for (int i = 0; i < 16; ++i) diff |= sig[i] ^ e2[i];
    return diff == 0;
}

The reconstruction works because s = k + e*d, so s*G - e*P is k*G + e*d*G - e*d*G = R; a verifier never needs the nonce or the signer's y coordinate. The three failure modes before the math, a character outside the alphabet, an overfull base85 group, s >= n, are rejected first so a corrupted or hostile value never reaches the curve.

6.2 Encryption

x: carries the sealed body and replaces m:.

t:message f:X1QZ3N d:X1RD89 ts:2026-08-08_14:26:40 x:pQ4m9xT2vB8kR sig:<60 characters>

131 bytes. t:, f:, d: and ts: stay in cleartext, so an intermediate station can route the packet, identify the recipient and release a carried copy on the matching receipt, without reading the content.

A later cipher suite takes a new key rather than changing this one.

The cipher. The sealed body is AES-256-CBC with PKCS#7 padding, under a key both ends compute and neither sends: the 32-byte X coordinate of the sender's private scalar times the recipient's public key (static-static ECDH on the curve of section 3), used as the AES key as it is. A fresh random 16-byte initialisation vector comes first, and x: is the base64url, without padding, of the vector followed by the ciphertext. The recipient takes the sender's public key from its t:identity (section 6.3), or from k: where the packet carries one. This is the scheme section 29.2 sizes, and every x: in this document is sealed with it. A body of P bytes seals to 16 + 16 * (floor(P / 16) + 1) bytes: 43, 64, 86, 107 and 128 characters for bodies of up to 15, 31, 47, 63 and 79 bytes.

6.2.1 Hiding parts of a message

x: seals a whole body. This section hides PARTS of one, the way a released government document does: the reader sees bars where the secrets sit, how long they are and where, and everything around them stays readable. Those with the passphrase recover the hidden parts; everyone else still gets a useful packet.

The author marks each secret in double parentheses, in the text or inside a field's value:

m:meet ((Max)) at ((pier2))
pos:38.7((223)),-9.1((393))

On the wire, every marked span becomes a run of the block character █ (U+2588), one per hidden character, in place:

m:meet ███ at █████
pos:38.7███,-9.1███

Redaction is visible wherever it happens, which field, where in the value, and how long. A bar costs 3 bytes in UTF-8, which the author is paying for the look.

Parsers learn the bar. When interpreting a value, a reader removes the block characters first and reads what remains against the declared type as usual (an amendment to the reading rule of section 4.3, and the only one). pos:38.7███,-9.1███ therefore parses as 38.7,-9.1 for everyone, a valid position, good to about ten kilometres, while the passphrase recovers the metre scale. The author decides what the stripped value says, because the author decides what stays outside the parentheses. A value still invalid after the bars are removed is skipped like any malformed value. Display keeps the bars; interpretation drops them.

The hidden parts travel in xr:, built like this:

  1. The plaintext is -> followed by the hidden pieces, one line per bar run, IN PACKET ORDER: field runs first, in the order the fields appear and the runs appear within each value, and the m: runs last, which is the packet's own order, since m: is last by grammar. Nothing else is carried: no field names, no offsets, no lengths. The wire's bars already say where and how big.
  2. The key is derived slowly and salted: key = first 16 bytes of PBKDF2-HMAC-SHA256(passphrase, "xprs-xr" || nonce, 100000 iterations). The nonce is fresh per packet, so a fresh derivation is paid per message even by someone who knows the passphrase, a fraction of a second for the reader, ruin at scale for a harvester or a brute-forcer. This is why there is one profile and no strength levels: the derivation is the strength, and it costs everyone the same per message.
  3. The cipher is AES-128-CTR: 12-byte random nonce, 32-bit big-endian block counter starting at zero.
  4. xr: = base64url, no padding, of nonce || ciphertext. Fourteen bytes of fixed overhead plus the secrets.

Decryption succeeds when the plaintext starts with ->. That sentinel detects the RIGHT KEY and nothing else; tampering is the signature's job, because sig: covers the bars and xr: like every other field. A packet whose signature verifies but whose decryption yields garbage was not forged, the passphrase was wrong.

Restoration is defended twice. Line i substitutes bar run i, and a line must have exactly the run's character count, a hundred-letter line cannot fill a three-bar hole. After substitution the field's value must still pass its type check. A restoration failing either test is discarded and the barred wire value stands. The blob can only ever fill the holes the wire visibly declares, at the sizes the wire declares.

The default passphrase is ################ (sixteen number signs), and the spec is plain about what it buys: obfuscation, not secrecy. Anyone can decrypt it, but each message still costs the full derivation, which is what keeps a bot from harvesting a thousand redacted email addresses for free. A secret that matters takes a real passphrase, and releasing a passphrase later is an ordinary message or file, needing nothing new.

Where bars are permitted. The rule: a field that third parties act on, routing, relaying, custody, reassembly, verification, transfers, channel tuning, is never redacted; a field that only informs the reader may be. Bars also only belong where the stripped value stays honest, which excludes the enumerated words (half a word is no word).

Redaction is permitted in:

Fields What they are
m: the text
pos: alt: spd: h: o: acc: position and movement, the accuracy dial
dest: a coarse destination still steers a carried packet (9.4)
temp: hum: press: wind: wdir: intemp: inhum: weather readings
batt: dose: lifedose: radon: rf: efield: mfield: odometer: telemetry readings
produces: consumes: grid: storage: charged: load: lifeproduces: lifeconsumes: lifegridin: lifegridout: lifeload: energy readings (section 15.5.2)
tag: title: name: content labels; unfindable without the key is the author's choice
price: onboard: information between people

Redaction is refused everywhere else, and a receiver ignores bars found where they are not permitted (the stripped value is read; nothing is restored there). The refusals, by reason:

A worked packet, with the nonce fixed to 000102030405060708090a0b so every value reproduces (a real sender draws it randomly). Author input as above; four bar runs in packet order; plaintext ->223, 393, Max, pier2 on four lines; derived key e7d6ef612e71fb09fd65dc71efd832c7:

164  t:message f:X1QZ3N d:X1RD89 pos:38.7███,-9.1███ ts:2026-08-18_17:00:00 xr:AAECAwQFBgcICQoL5tqwc_xiDDNhLVD9YLDyKZnrvw m:meet ███ at █████

Everyone in range reads a meeting near 38.7,-9.1 between two stations, with the position visibly coarsened and two words visibly withheld. Holders of the passphrase read who, which pier, and where to ten metres.

The bars leak the LENGTH of a secret, deliberately, that is the redacted-document look doing its job, and an author who must hide length pads inside the parentheses before marking. Text larger than a packet is a file (section 7.7) and redacts the same way; nothing new travels.

6.3 Identity

t:identity f:X1QZ3N ts:2026-08-08_14:26:40 k:npub1qz3n7fu9j9uenmyva7ha6x9eqwymytv2847ccv4vxdmn45y50q7h7k5f sig:<60 characters>

171 bytes. A receiver stores the binding and uses it to verify signed packets from that callsign. Answers q:identity.

An identity announcement is signed like everything else. An earlier draft of this document said it was not, on the grounds that the signature would have to be verified with the key the packet carries and was therefore circular. That reasoning was wrong, and the nickname below is what exposes it.

A self-signature proves the sender holds the private key, which is not circular and is not nothing. Without one, anybody can rebroadcast t:identity f:X1QZ3N k:<the real key of X1QZ3N> with whatever else they like attached: the callsign still derives correctly from the key (section 3), every check passes, and the extra fields are the attacker's. With one, they cannot, because they do not have the private half.

What the signature does not establish is entitlement to the callsign. For an X1, X2 or X3 callsign the derivation in section 3 does that. For a callsign issued by an authority nothing in this format does: section 29 proves that the key holder is present, and section 6.4.2 says where entitlement is checked instead, which is the authority's own register and not any packet.

6.3.1 Nicknames

nick: gives a callsign a human-readable name.

t:identity f:X1QZ3N ts:2026-08-08_14:26:40 k:npub1qz3n7fu9j9uenmyva7ha6x9eqwymytv2847ccv4vxdmn45y50q7h7k5f nick:joao sig:<60 characters>

181 bytes. One to sixteen characters: ASCII letters, digits, - and _. No spaces, because no value except m: may contain one, and no accents, because the whole format is ASCII. A name that needs more than that belongs in a profile somewhere else, and this field is a label rather than a biography.

Three rules, and the third is the one that matters.

It only counts when the signature verifies. A receiver that cannot check the signature shows the callsign and not the nickname. An unsigned or unverifiable nickname is a claim by nobody.

The newest verifiable announcement wins, decided by ts:. A station changes its nickname by announcing again.

A nickname is never an address. d: takes a callsign and only a callsign. Nicknames are not unique, cannot be made unique without a registry this format deliberately does not have, and two stations calling themselves joao is expected rather than exceptional. A receiver that let a user address a nickname would have built the spoofing surface that signing the rest of this section was meant to close.

Show it as decoration next to the callsign, never instead of it.

A callsign of another network (section 3.2) is the one exception, because nothing about it can ever be verified. The gateway passes on the name that network gives its node, reduced to this field's characters, in an unsigned t:identity:

110  t:identity f:MTA1B2C3D4 ts:2026-09-19_12:04:00 scope:regional nick:Maria-boat zmid:a1b2c3d41581bdbe via:X3DCK0

A receiver may show it, marked as the other network's own name for the node, and must not show it for an X callsign in any circumstance: there an unsigned nickname is still a claim by nobody.

6.3.2 A face and a line about yourself

A townhall of callsigns is a spreadsheet. file: gives an identity a picture and m: a line of description, both optional and both signed with the rest:

t:identity f:X1QZ3N ts:2026-08-08_14:26:40 nick:joao file:nYxKzGm4vT2pQ8dW5jR7cL0aFbNs9hUe3oXiC6EkM1w.jpg sig:<60 characters> m:sailing the Algarve coast

198 bytes. The picture is a reference and not bytes, a content hash like any other file in this format (section 7.7), fetched with cmd:file (section 11.2) if the receiver wants it and ignored entirely if it does not. A station that never fetches an avatar has lost nothing but a picture.

An identity announcement carries any subset of these fields, and a receiver keeps, for each field, the value from the newest verifiable announcement that carried it. That rule is forced by arithmetic rather than chosen: the key binding and the decoration together come to 262 bytes, which does not fit.

181  t:identity f:X1QZ3N ts:2026-08-08_14:26:40 k:npub1qz3n7fu9j9uenmyva7ha6x9eqwymytv2847ccv4vxdmn45y50q7h7k5f nick:joao sig:<60 characters>
198  t:identity f:X1QZ3N ts:2026-08-08_14:26:40 nick:joao file:nYxKzGm4vT2pQ8dW5jR7cL0aFbNs9hUe3oXiC6EkM1w.jpg sig:<60 characters> m:sailing the Algarve coast

The decoration itself may be split the same way, for the same reason: the example above is 198 bytes, and the smallest advertisement a radio controller in the field carries is 184 (section 7.7.4's budgets are per packet, and an oversized frame is refused rather than truncated). A station whose decoration does not fit sends nick: and file: in one packet and m: in another; the rule above already reassembles them, field by field.

- clears a field. file:- means "no picture", nick:- "no nickname" and m:- "no description". Without it the rule above could never take a picture back: an announcement that leaves file: out keeps the old value, by design. A station states every decoration field it has, with - for the empty ones, so a removal travels like any other change and loses to a newer value like any other value.

t:identity f:X1QZ3N ts:2026-08-09_09:00:00 nick:joao file:- sig:<60 characters> m:-

The split turns out to be the right shape anyway. The key binding is small and must be repeated often, because a receiver that has never heard it can verify nothing (section 20.1). The decoration is larger and changes once a year, so it goes out rarely, which is section 30 applied to this format's own traffic.

A packet without k: still verifies, against the key the receiver already holds for that callsign. One from a station whose key is unknown is ignored, exactly as the nickname rule above requires.

6.4 Permitted use by band

Spectrum Callsign in f: Signing Encryption
Licence-free (Bluetooth and LoRa ISM, WiFi), and the internet any, including a self-generated X1, X2 or X3 permitted permitted, and is the default for direct messages
Licensed spectrum, including the amateur bands only one issued by a competent authority to the operator transmitting permitted not permitted on amateur bands

Amateur regulations prohibit obscuring the meaning of a transmission. A licensed operator using XPRS on amateur bands is bound by that rule as on any other mode and must not transmit x: there.

A signature is not encryption. It leaves the text in cleartext and establishes only authorship, so signing is permitted on amateur bands.

An implementation able to reach amateur infrastructure must refuse to transmit a sealed body onto it.

6.4.1 Only an issued callsign may transmit on licensed spectrum

Transmitting XPRS on a licensed frequency requires a callsign issued to the operator by a competent authority, and nothing else will do. This is the one rule in this document that is not ours to relax: it comes from national regulation, it applies to the person keying the transmitter, and no property of the format changes it.

An X1, X2, X3, X4 or X5 callsign is derived by its holder from its own key (section 3). No authority issued it, no register lists it, and it can be generated by anyone in a moment. That is as it should be on licence-free spectrum, where a callsign is a label. On licensed spectrum it identifies nobody, which is the thing an identification requirement exists to prevent, so a packet whose f: is X1, X2, X3, X4 or X5 must never be originated onto a licensed frequency. A licensed operator transmits under the callsign on their licence, which the format already accepts at any length and with a suffix.

This binds gateways harder than it binds people, because a gateway does it automatically and at volume. A station bridging licence-free traffic onto licensed spectrum must drop packets from self-generated callsigns rather than relay them. Relaying one puts an unidentified transmission on the air under the gateway operator's licence, and the operator, not the sender, answers for it.

6.4.2 Associating an issued callsign with a key

An operator who wants their traffic to be verifiable under their real callsign announces the binding themselves, with the identity packet of section 7.3 and their own callsign in f::

t:identity f:CT1ABC ts:2026-08-08_14:26:40 k:npub1qz3n7fu9j9uenmyva7ha6x9eqwymytv2847ccv4vxdmn45y50q7h7k5f sig:<60 characters>

171 bytes. From then on their packets can carry sig: and a receiver can check that the operator who announced that key wrote them.

One key may bind several issued callsigns. Operators who are licensed in more than one jurisdiction hold more than one: CT1ABC at home, EA/CT1ABC across the border. Each is bound by its own identity packet, all with the same k:. A packet still carries exactly one callsign in f:, the one the operator chose for it. An implementation keeps one of them as the default and SHOULD let a conversation keep the callsign it began under. A reply to a 1:1 addressed to EA/CT1ABC goes out as EA/CT1ABC, and a receipt names the callsign the message was addressed to. A station answers to every callsign it has bound. Section 6.4.1 applies to each callsign separately, in the jurisdiction that issued it. Choosing the callsign that is lawful for where the transmitter stands is the operator's act, never inferred from a position.

Be exact about what this proves, because the temptation to read more into it is strong. The self-signature proves the sender holds the private key for the announced npub. It proves nothing whatever about entitlement to CT1ABC: an X1 callsign is checked against its key by arithmetic, an issued callsign has no arithmetic relationship to any key, and this format has no registry and issues no credentials. A second station can announce the same callsign with a different key, and both announcements will verify.

Question Answered by
does the holder of this key write these packets the signature, section 7.1
is that key holder present right now a challenge, section 29, on licence-free spectrum only
was this callsign issued to this person nothing in XPRS; the authority's public register, out of band

The last row is the honest one. Most administrations publish a searchable licence register, and checking a name and locality against it is a human act performed once, not a protocol exchange. What XPRS contributes is the part that register cannot give you: that this packet, now, came from the same key as the one you checked.

A receiver that has verified a binding out of band may mark it as such in its own records. It must never transmit that verdict as though it were a fact about the callsign, because a claim that a callsign is licensed carries far more weight than the sender's opinion deserves.

6.4.3 What signing costs on the amateur bands

Amateur regulations prohibit obscuring the meaning of a transmission, and a signature does not obscure it. sig: is detached: the message stays in clear in m:, legible to any receiver, and the signature sits beside it as evidence about authorship. Anyone monitoring reads the traffic exactly as they would unsigned.

t:message f:CT1ABC d:G0XYZ ts:2026-08-08_14:26:40 sig:<60 characters> m:net starts at eight on the repeater

152 bytes, lawful on an amateur band, and verifiable.

Three consequences follow, and they are the price of operating there:


Part II. Messaging

Saying things and asking things: one-to-one and group correspondence, how a packet is carried to someone who is not here, closed groups, and the command verbs.

7. Messages

7.1 Broadcast

No d:. The packet is addressed to whoever is in range.

t:message f:X1QZ3N ts:2026-08-08_14:26:40 m:anyone near the north gate?

71 bytes.

7.2 Direct

t:message f:X1QZ3N d:X1RD89 ts:2026-08-08_14:26:40 m:meet at the bridge at six

78 bytes, identifier de9780.

7.3 Group

d: holds a group name. Group names are uppercase, 1 to 16 characters. A station tells an open group from a person or a machine by the X prefixes of section 3 and the characters that follow, so an open group may not be named like one of those, at any of the lengths section 3 allows, which is why a four-character group name such as X1AB is now as unusable as X1ABCD was. Nor may it look like a callsign of another network (section 3.2): MT or MC and eight hexadecimal digits.

t:message f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 m:net starts in ten minutes

78 bytes, identifier 399227. Anyone may post to LISBOA and nobody may be removed from it, which is the whole of what an open group is.

A group that needs a member list holds a keypair and is addressed by the X5 callsign derived from it (section 10). Everything in this section applies to it unchanged:

t:message f:X1RD89 d:X5A3F2 ts:2026-08-08_14:26:40 m:net starts in ten minutes

78 bytes, identifier 89a9c8. The same size as the line above it, because a closed group costs an ordinary packet nothing.

7.4 Replies

r: names the packet being replied to, which may be any packet carrying content (section 7.5).

t:message f:X1RD89 d:LISBOA ts:2026-08-08_14:36:00 r:399227 m:I'll be late, start without me

92 bytes. The reply has its own identifier, computed the same way, so it can be replied to in turn. A receiver that has not seen the parent still displays the reply, marked as answering a message it does not hold.

Below the first level a reply also names the thread it belongs to, with root::

t:message f:X32DVA d:LISBOA ts:2026-08-08_14:40:00 root:399227 r:8536fb m:no rush, we start at nine

99 bytes. r: keeps exactly the meaning it had, the packet being replied to, and root: names the packet the whole conversation hangs from.

A first-level reply carries r: alone, because its parent is the root. Only deeper replies carry both, so the eleven bytes are spent only where they buy something.

What they buy is the ordinary case on this network rather than an unusual one. A lost middle packet orphans everything beneath it: with r: alone, a receiver holding a reply whose parent it never heard cannot tell which conversation the reply belongs to, and a thread arriving out of order over three bearers reassembles into fragments. root: makes that reply filable anyway, shown under the right conversation, in the right place, with a gap where the missing packet was.

7.5 Reactions

t:reaction f:X32DVA d:LISBOA r:399227 add:like
t:reaction f:X32DVA d:LISBOA r:399227 remove:like

46 and 49 bytes. add: states what is being added and remove: withdraws that same thing, so neither has to be read as the negation of the other. A reaction carries no m:. It is counted once per callsign, is idempotent, is not displayed as a message and raises no notification.

7.5.1 Replying, quoting and passing on

Three things a social network needs, and the format already had two of them.

A reply is r: (section 7.4), and it works on any packet carrying content.

A quote is a reply that says something. There is no separate mechanism and there does not need to be: r: names what is being quoted and m: is the comment on it.

t:status f:X32DVA ts:2026-08-08_14:40:00 r:399227 m:worth reading, he is right about the feed point

99 bytes. A client renders the parent above the comment; a client that never heard the parent shows the comment and says so, which section 7.4 already requires of every reply.

Passing something on unchanged is add:repost.

t:reaction f:X32DVA d:LISBOA r:399227 add:repost
t:reaction f:X32DVA d:LISBOA r:399227 remove:repost

48 and 51 bytes. It is a reaction because it has a reaction's shape exactly: one per callsign, idempotent, withdrawable, and no text of its own.

What travels is the original packet, not a copy of it. A repost says "this belongs in front of the people who follow me", and the thing itself is re-aired with f:, ts: and sig: untouched, the same act as a history replay (section 11.2.1), and safe for the same reason: duplicates collapse on the derived identifier, so a post reposted by nine stations is still one post.

That is the difference from a quote worth understanding. A repost adds nothing and changes nothing, so it cannot misrepresent what it carries; the signature still proves who wrote it and the reposter's callsign appears only on the reaction. A quote is the sender's own packet with their own words, and they answer for those.

A reply and a reaction are different acts, and not every packet takes both.

Every packet has an identifier (section 5), so r: can name any of them. What differs is whether naming it means anything:

Packet type Reply React
message, status, blog, observation, track, passage, event, offer, need, channel, service, place, poll, file, warning, info yes yes
sos yes no
reaction, receipt, request, challenge, response, identity, mailbox, command, result, moderate, report, ping, pong no no

A weather observation, a warning, a blog post, an offer and a channel announcement can all be replied to and reacted to. That is what deriving an identifier for every packet rather than only for those carrying a message.

A call for help takes replies and never reactions. Answering an sos is t:receipt with s:ack (section 33.5), which says a station heard it and is a different thing from approving of it. A reaction adds nothing that mechanism does not already carry, spends airtime on a channel that must stay clear, and a counter of likes under somebody's call for help is grotesque. A receiver discards one.

The bottom row is protocol machinery rather than content, and a receiver ignores a reply or reaction naming any of it.

A reaction is excluded because a reaction to a reaction is not a thing anyone means, and a reply to one is a reply to the wrong packet: the reaction already names what it was about, and that is what should be answered.

A track point is replied to and reacted to like anything else, but people mean the track rather than the point. A receiver attributes both to the track, which f: and track: identify (section 21), and shows them once against the line instead of against seq:7.

A challenge and its response are excluded for a stronger reason. They are a two-party authentication exchange, valid for sixty seconds, and the only thing that should ever name a challenge is its own response. Anything else pointing at one is noise at best, and at worst an invitation to treat a security exchange as a conversation.

7.5.2 Mentioning somebody

A mention is @ followed by a callsign, written in the text where a person would write it:

t:status f:X1QZ3N ts:2026-08-08_14:26:40 m:thanks @X1RD89 and @X32DVA for the relay last night
t:message f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 m:@CT1ABC-9 is the one to ask about the repeater

94 and 99 bytes. There is no key for this, and there should not be: a field would carry the same information twice, once for the parser and once for the reader, and cost bytes to do it. A receiver that has never heard of mentions still shows @X1RD89 and a person still understands it, which is the property design rule 2 exists to protect. Other formats need a tag because their content is opaque to the protocol; m: is not opaque to anybody.

The parse rule is one line. @ followed by one or more call characters, uppercase letters, digits, - and /, ending at the first character that is not one of those. A trailing - or / is punctuation and not part of the callsign.

Uppercase is what makes it safe. Callsigns are always uppercase (section 3), so write to me@example.com contains no mention, and the rule needs no escape character, no delimiter and no exception.

There is no limit on how many. The packet limit is already the limit, and it is a better one than any number this document could pick: mentions compete with the message for the same 250 bytes, so a station that tags thirty people has no room left to say anything to them.

Two things follow. A station that finds its own callsign after an @ raises a notification, which is the entire point of the convention. And anyone may mention anyone: there is no consent, no blocking and no way to prevent it, so a client must let its operator mute a callsign whose mentions it does not want.

A mention is also the strongest signal a station has for deciding what to keep (section 30.3). Being named is what makes a packet worth more than the traffic around it, and until now nothing in this format said so.

7.6 Long messages

A message longer than one packet is split into numbered parts. Every part carries the same ts: and its own n:.

t:message f:X3RLY7 d:LISBOA ts:2026-08-08_14:26:40 n:1/3 m:The repeater on the hill is down.
t:message f:X3RLY7 d:LISBOA ts:2026-08-08_14:26:40 n:2/3 m:We swapped the antenna feed this morning
t:message f:X3RLY7 d:LISBOA ts:2026-08-08_14:26:40 n:3/3 m:and it is back up, but only just.

92, 99 and 92 bytes.

7.7 Files

file: is the SHA-256 digest of the file contents as 43 base64url characters (no padding), a dot, and 1 to 8 lowercase alphanumeric characters giving the type.

t:message f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 file:nYxKzGm4vT2pQ8dW5jR7cL0aFbNs9hUe3oXiC6EkM1w.jpg m:the antenna after the storm

133 bytes. The caption is an ordinary m: field.

base64url because the digest is the single most repeated expensive value in the format and 43 characters against 64 for hex is 21 bytes returned to every packet that carries one. A receiver also accepts the digest as 64 lowercase hexadecimal characters, the earlier form of this field, and treats the two as the same reference; a sender emits base64url.

The hash identifies the file exactly, so any station holding those bytes can satisfy the reference and a receiver can verify what it obtained. The extension is advisory: it indicates how to present the content and never affects identification. A receiver that does not recognise an extension offers the file as an opaque download.

How the bytes are transferred is outside this specification. A reference remains valid whether the file arrives over the same radio, over the internet, or on physical media. What THIS document defines is everything around the bytes: how a file is described (7.7.1), how a large one is verified in pieces and a folder of them is listed (7.7.2), how a whole folder is synchronised (7.7.3), how a small one rides the packets themselves (7.7.4, and 7.7.6 for one too big for that), how anyone asks who holds one (section 8, q:have), and how one is fetched or deposited (section 11.2, cmd:file and cmd:put).

So a file reaches a station one of three ways, chosen by size and by what the pair's bearers will carry:

bytes the way where it is defined
up to 896 inline in the packet, b: split by n:X/Y 7.7.4
up to the low tens of kilobytes chunked by offset across ordinary t:file packets, no link, the receiver's have: map bringing what was lost 7.7.6
anything the bulk lane: cmd:file and its t:result bracket the transfer, a bearer's own byte protocol moves the bytes in the middle 11.2, 11.2.2

A file shared in a conversation is where those three meet, and 7.7.7 writes that case down whole: the reference beside the caption rather than inside it, size: so the receiver can decline, a preview plus a t:file description where the original is too large for the lane that reaches the far station, and when a station may fetch without being asked.

The second exists for the bearer that carries packets and nothing else: a shared internet transport that forwards directed messages and drops a link, a radio too small for a session. The third is the one to want wherever a link exists, since the packet grammar is a poor pipe for bulk.

7.7.1 Saying what a file is

A hash identifies a file perfectly and describes it not at all. t:file is the packet that says what the bytes are, so somebody who does not already know can decide whether to want them:

t:file f:X1QZ3N ts:2026-08-08_14:26:40 file:nYxKzGm4vT2pQ8dW5jR7cL0aFbNs9hUe3oXiC6EkM1w.jpg size:240kB tag:radio m:the finished dipole, feed point centred

154 bytes. m: describes it, tag: files it under topics, and size: is a quantity with its unit like every other measurement in this format.

size: is the field that earns its bytes. cmd:file (section 11.2) asks a station to send the content, and on a bearer that owes seconds of silence per packet the difference between a 240 kB photograph and a 40 MB video is the difference between a fetch and a mistake. Knowing the size first is how a station declines politely instead of starting something it cannot finish.

Two optional fields complete the description:

t:file f:X1QZ3N ts:2026-08-08_14:26:40 file:nYxKzGm4vT2pQ8dW5jR7cL0aFbNs9hUe3oXiC6EkM1w.jpg size:240kB ph:qA7dTf2mWx9bK4pZcV0yLuJ3gRhN8sE5iDoQ6vXaB1M.xfl tag:radio sig:<60 characters> m:the finished dipole

250 bytes, a fully described, signed, piece-verifiable file sits exactly at the packet limit. Adding name: pushes it over, and that is fine: a t:file splits like any packet (section 7.6), and a description is not beacon traffic.

A description packet is also the only thing that makes a file findable by words. file: alone can be looked up by somebody who already has the hash and by nobody else; a t:file gives a station something to index, so "that photo of the dipole" is a question with an answer.

t:file describes and never delivers. The bytes travel however they travel (above), and a description whose content nobody holds is a description of something lost, worth keeping anyway, because it says what was lost.

7.7.2 Listings: a folder's files, and a file's pieces

One text format serves two needs, because a folder of files and a file's pieces are the same shape: a list of hashes with sizes. A listing is a UTF-8 text file of LF-terminated lines:

XFL1
Uc3nRw8kFa5xPd1qGz7mYb0tJe6vHs2iLoA9XfCqK4E.gpx 18kB ridge track.gpx
nYxKzGm4vT2pQ8dW5jR7cL0aFbNs9hUe3oXiC6EkM1w.jpg 240kB dipole finished.jpg

The first line is the magic. Every other line reuses the grammar the packets already have: a reference (or a bare 43-character hash), a qty size, and, because names contain spaces, the name last, running to the end of the line, exactly the rule that puts m: last in a packet.

A listing is itself an ordinary content-addressed file: described by a t:file, fetched with cmd:file, asked after with q:have, deposited with cmd:put, and verified against the reference that named it. Nothing new travels.

A station that verified pieces 0 to 411 of 900 holds pieces 0 to 411, may say so (section 8), and may serve them. That is the rule that turns a crowd of partial downloads into a working swarm: nobody has to finish before being useful, and the most-copied pieces of a popular file are available from many stations at once.

7.7.3 Syncing a folder

A folder is published by describing its listing, the existing t:file with the reused kind: field, and nothing new:

t:file f:X1QZ3N ts:2026-08-08_14:26:40 file:qA7dTf2mWx9bK4pZcV0yLuJ3gRhN8sE5iDoQ6vXaB1M.xfl kind:folder count:34 size:210MB sig:<60 characters> m:Trip photos 2026

207 bytes. count: says how many files, and size: is the folder's TOTAL payload, so a station knows what "everything" costs before fetching anything.

The whole flow is vocabulary this document already has. Fetch the listing (cmd:file on the .xfl reference), read the names, sizes and hashes, then fetch any or all members (cmd:file per reference, q:have to find nearer holders first). Every member is verified against its own hash on arrival, so a folder assembled from six different stations is exactly the folder that was published.

Sync is snapshots. A changed folder is a new listing with a new hash, announced with a new t:file. Files that did not change keep their hashes, so a receiver holding the previous snapshot diffs two listings line by line and fetches only what is new, incremental synchronisation with no mutation protocol, no version numbers and nothing to negotiate. The old listing remains a valid description of the old folder, which is what an archive wants anyway.

7.7.4 A small file inline

Below a certain size, describing a file costs more than sending it, the rule of section 34.2, "send whichever is smaller", applied to content. b: carries the file's bytes as base64url in the t:file that describes it:

t:file f:X1QZ3N ts:2026-08-08_14:26:40 file:Uc3nRw8kFa5xPd1qGz7mYb0tJe6vHs2iLoA9XfCqK4E.png size:240B b:iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8z9AAAAAdklEQVQ4y2P8z8BQ

A larger inline file splits per section 7.6 with one added rule: b: splits like m:, but the parts are joined with nothing, base64url contains no spaces, and a space would corrupt it. A 240-byte icon, signed, in three parts:

t:file f:X1QZ3N ts:2026-08-17_10:00:00 n:1/3 file:Uc3nRw8kFa5xPd1qGz7mYb0tJe6vHs2iLoA9XfCqK4E.png size:240B b:PtYgjmUhBel31iEl2hpChYgCfrL1spNxnyVmihA_2O76UMFxFkM_R5Kjp1vRt-1fjORS_6ilI8ihN5KXSc7Tvo_hBKqFYY_kv5ZJr3J1TWDtkwtDDb-xHKas1VOqg6YYZYn9ZhyiA4uo
t:file f:X1QZ3N ts:2026-08-17_10:00:00 n:2/3 file:Uc3nRw8kFa5xPd1qGz7mYb0tJe6vHs2iLoA9XfCqK4E.png size:240B b:RgnatmUdjAWtGSU8po-799NksnRH9ucAUsdMlHUvTCQCyEZDz_TddJ8HyS5SUkCnD8zRA9a9SkpXz9w3QlY7Zkuvqdt7s8Stqcbnr3yBdGBLEPH1qhT61qtc4xatws8phP9nhFyJfm5d
t:file f:X1QZ3N ts:2026-08-17_10:00:00 n:3/3 file:Uc3nRw8kFa5xPd1qGz7mYb0tJe6vHs2iLoA9XfCqK4E.png size:240B sig:<60 characters> b:i4PzJ59FHz5r1pY4OjE2jBMptUsGr7CmY-uCu3ZR

250, 250 and 215 bytes. Every field except b: and n: repeats on every part, so any single part says whose file, which file and how big. Reassembly is the section 7.6 procedure with the empty join: concatenate the b: values in n: order, decode, hash, compare against file:. There is no offset and no per-part checksum, because n: is the index and the whole-file hash is the only integrity that matters at this size.

With these fields a part carries 140 characters of b: and the signed last part 75, so nine parts carry 1195 characters: an inline file tops out at 896 bytes. Thumbnails, avatars, QR payloads, keys and configuration fit; anything larger travels as section 7.7 says. The two never mix, a file is inline or it is fetched, and a receiver that decodes b: checks it against file: and, on a match, holds the file like any other holder.

The reason this lane exists is the bearer that has no other: on a LoRa-only network there is no bulk connection to move bytes over, and a packet is the only vehicle there is. 896 bytes is a real photograph thumbnail or a real public key, and either arriving over 40 km of nothing is worth nine packets.

7.7.5 The BitTorrent bridge

A file can additionally be offered to stock BitTorrent clients, and the trick is that nothing extra needs to be transmitted to arrange it. The torrent for a file is built deterministically: single file, named <digest-hex>.<ext>, piece length the power of two nearest size/1024 clamped between 16 kB and 4 MB, no private flag and no source field. Two stations that hold the same bytes therefore derive the same torrent and the same infohash independently, the swarm address is a pure function of the content.

ih: (40 hexadecimal characters) carries that infohash when talking to something that cannot derive it, a link handed to somebody's ordinary torrent client. Between XPRS stations it is dead weight and is not sent.

The SHA-256 remains the identity throughout. The infohash addresses a swarm; whatever the swarm delivers is verified against file: like bytes from any other source, and fails like them if it lies.

7.7.6 A small binary over the packet lane

Section 7.7.4 puts a file inline with b: split by n:X/Y, and that grammar allows at most nine parts (section 4.3, a ratio is 1..9), so it tops out at 896 bytes. That is enough for a thumbnail or a key and not for a meme. Yet the case that needs it most is real: a shared transport carries directed messages and identity announces and DROPS the bulk lane, a public Reticulum hub that cross-forwards text between its clients but not a binary link or resource (section 12.12.1). The GATT/resource byte lane never completes there, and a small image has one vehicle left, the packet itself.

For a file above 896 bytes and up to a small cap, chunk the RAW bytes by offset across ordinary t:file packets:

t:file f:X1QZ3N d:X1RD89 ts:2026-08-08_14:26:40 file:Uc3nRw8kFa5xPd1qGz7mYb0tJe6vHs2iLoA9XfCqK4E.png size:5000 off:0 b:iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8z9A

off: is the byte offset the chunk begins at, the same field cmd:file resume already uses (section 11.2); b: is base64url of the raw chunk; size: is the whole length, repeated on every packet so any one identifies the transfer. Every chunk but the last has the same length: the largest multiple of three bytes whose packet, with off: written as the file size, still fits 250. So chunk k begins at k times that length, and any full chunk tells a receiver the grid. The receiver places each chunk at its offset and, once every byte of size: is present, decodes nothing further, hashes the assembled bytes and compares against file:.

The integrity is the whole-file hash and nothing else, the rule section 7.7.4 already states at this size: a wrong or forged chunk simply makes the assembled sha not match, and the file is discarded whole. There is no per-chunk signature, because signing hundreds of tiny packets would cost more than the transfer and buy nothing the content hash does not already give. Nor is there an acknowledgement per chunk: the sender sends them all, paced, and waits for nothing. What is missing is the receiver's to say. Chunks arrive tens of milliseconds apart when they arrive at all, so a few seconds without one is a lost packet, not a slow one, and the receiver then asks with cmd:file carrying have:, its chunk map in section 8.1's form, bit k for chunk k:

193  t:command f:X1RD89 d:X1QZ3N ts:2026-08-08_14:27:12 cmd:file file:Uc3nRw8kFa5xPd1qGz7mYb0tJe6vHs2iLoA9XfCqK4E.png have:-_3_____Hw sig:<60 characters>

The holder re-sends only what the map lacks, as plain chunks again, and answers 202, or 200 when the map is already whole, or 404 when it no longer has the bytes, which tells the receiver to stop. The asks back off and stop on their own, so a sender that is gone is not asked forever. A whole map of the largest file this lane allows is under sixty characters; off: alone still resumes from an offset, exactly as a dead bulk transfer resumes. A receiver bounds both the file size it will assemble and the number of transfers it will buffer, so a peer that announces a large size: and never completes it cannot grow memory without bound.

This lane is for SMALL binaries only, a firm cap in the low tens of kilobytes, and it never mixes with the bulk lane: a file is fetched with cmd:file where a link exists, and packetised this way only where one does not. A meme at a few hundred packets over a hub that would otherwise pass nothing binary is the whole point; a video is section 7.7's business and stays there.

7.7.7 A file in a conversation

A picture shared in a conversation is the ordinary case of everything above, and it is worth writing down as one thing, because three sections meet in it.

The reference is a field, not caption text. file: sits in the packet beside m:, never inside it:

143  t:message f:X1QZ3N d:X1RD89 ts:2026-08-08_14:26:40 file:nYxKzGm4vT2pQ8dW5jR7cL0aFbNs9hUe3oXiC6EkM1w.jpg size:23kB m:the antenna after the storm

That placement is what lets a sealed 1:1 (section 9.2) carry an attachment at all: the caption goes into x: and the reference does not. A hash is public information in this format already, since section 11.2 gates the BYTES and never the reference, so nothing is given away by leaving it in the clear that was not given away by asking a stranger for the file.

Only m: splits (section 7.6). A caption long enough to need parts repeats file: on every one, so any single part identifies the attachment.

size: SHOULD accompany file: on a message. It is the same field for the same reason as in 7.7.1: it is what lets the receiving station decline politely. Without it a receiver either fetches blind or does nothing, and the first of those is how a shared radio gets jammed by a video nobody asked for. name: follows where the packet has room for it.

A picture larger than the packet lane travels twice. The lane that crosses a public hub (7.7.6) tops out in the low tens of kilobytes, and a photograph from a telephone is megabytes. A sender with a link may simply share the original; where the far station is reachable only through a hub that forwards packets and nothing else, the original would never arrive and the conversation would show a grey box. So the message carries a small preview of the picture, which the packet lane can carry anywhere, and a companion t:file describes the original:

143  t:file f:X1QZ3N d:X1RD89 ts:2026-08-08_14:26:41 r:399227 file:Uc3nRw8kFa5xPd1qGz7mYb0tJe6vHs2iLoA9XfCqK4E.jpg size:3.1MB name:storm-antenna.jpg

r: names the message the description belongs to (section 5), which is the same relationship a reply or a receipt uses. The receiver renders the preview immediately and knows, from the companion, that a 3.1 MB original exists and what it is called. Nothing new is invented for this: a preview is a file, the description is t:file, and the link between them is r:.

A preview is a courtesy, not a requirement. A file already small enough for the packet lane travels as itself, with one file: and no companion, and a station that makes no previews shares the original and nothing else.

Asking for the bytes. A receiver holding a reference and none of the content asks the sender with cmd:file (section 11.2). Over the packet lane the ask carries a have: map, and a receiver holding nothing sends a map of zeroes, which means "send everything":

185  t:command f:X1RD89 d:X1QZ3N ts:2026-08-08_14:27:12 cmd:file file:nYxKzGm4vT2pQ8dW5jR7cL0aFbNs9hUe3oXiC6EkM1w.jpg have:AA sig:<60 characters>

When to fetch without being asked. Bytes that arrive unbidden are bytes somebody else's transfer did not get, so the rule is about the bearer and not about the file:

A non-image obeys the same rules with no preview to soften them, so size: and name: are the whole of what the receiver has to judge by:

157  t:message f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 file:Uc3nRw8kFa5xPd1qGz7mYb0tJe6vHs2iLoA9XfCqK4E.pdf size:480kB name:mast-plan.pdf m:the plan for the mast

8. Asking and answering

q: says what the sender wants back. s: answers using the same words.

q:ack        confirm this reached the device
q:read       confirm the operator read it
q:pos        send your position
q:batt       send your battery level
q:identity   send your public key
q:sign       sign a receipt confirming you read this
q:pong       reply to this reachability test
q:have       say whether you hold the file named by file:
q:state      send your device state: state:, level:, target: (section 11.7)
q:snapshot   send a current still: url: to fetch it (cameras, section 11.7.2)
q:stream     send where the live view is: url: to open it (section 11.7.2)
q:mail       say how much mail you hold, for the callsign in only: (13.12.3)

Several are separated by commas. An unknown word is ignored, so q:pos,bat,co2 still returns position and battery from a station that has never heard of CO2.

Absence of q: means nothing is expected back, so silence is never ambiguous, with one exception, and it is the common case: a direct message between two stations that have exchanged one before is acknowledged without being asked (section 9.7.1). Everything else still answers only what q: requested.

t:message f:X1QZ3N d:X1RD89 ts:2026-08-08_14:26:40 q:ack,read m:did you get the keys?

85 bytes, identifier 9821a4.

The answers, naming that identifier in r::

t:receipt f:X1RD89 d:X1QZ3N r:9821a4 s:ack
t:receipt f:X1RD89 d:X1QZ3N r:9821a4 s:read

42 and 43 bytes. s:ack is sent when the message reaches the device, s:read when the operator reads it. A station that does not track reading sends s:ack only, and the sender sees exactly which of the two requests was satisfied.

A request for data is the same exchange without a message:

t:request f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:26:40 q:pos,batt
t:observation f:X3RLY7 pos:38.7810,-9.2043 batt:64% ts:2026-08-08_14:26:40 s:pos,batt

61 and 85 bytes. The answer is an observation like any other, so by default it is a broadcast: it carries no d:, and the asker reads it together with every other station in range (section 15). s: is what tells the asker this is its answer. A reading one station asked for is usually one its neighbours can use as well, and a broadcast answer saves the next one from asking. A station may still address the answer with d: when the reading concerns the asker alone, such as a lock telling its owner it is locked (section 11.7).

A station holding only part of what was asked says so, rather than failing:

t:observation f:X3RLY7 pos:38.7810,-9.2043 ts:2026-08-08_14:26:40 s:pos

71 bytes: position sent, battery not available, no error packet needed.

s:no is the one word not in q:, for a request a station will not or cannot serve at all:

t:receipt f:X3RLY7 d:X1QZ3N ts:2026-08-08_14:26:40 s:no

55 bytes.

8.1 Who holds a file

q:have with a file: reference asks who holds those bytes. Broadcast, it is the question a station asks the street before spending a fetch on somebody far away; directed, it checks one station before asking it to serve.

t:request f:X1QZ3N ts:2026-08-08_14:26:40 q:have file:nYxKzGm4vT2pQ8dW5jR7cL0aFbNs9hUe3oXiC6EkM1w.jpg

101 bytes, identifier 17d873. A holder answers, directed, with have: in one of three forms:

t:result f:X3RLY7 d:X1QZ3N ts:2026-08-08_14:26:41 r:17d873 s:have have:full size:240kB
t:result f:X3RLY7 d:X1QZ3N ts:2026-08-08_14:26:41 r:17d873 s:have have:412/900 size:58MB

86 and 88 bytes. have:full is the whole file. A partial holder answers the piece bitfield as base64url, least significant bit first, when it fits the packet, up to roughly 1200 pieces, and the fraction have:412/900 when it does not; the exact map travels with the transfer itself once one starts. A station that holds nothing stays silent: on a broadcast ask, a hundred "no"s would cost more than the answer is worth, and silence already says it (section 8).

This is the radio's version of the claim a provider record makes on an internet overlay: the same "I hold it", scoped to whoever can actually hear the speaker, which for a fetch over the street is the right scope.

Any station may act on a receipt it overhears. A station holding a message for later delivery discards its copy on hearing the matching s:ack whose signature it has verified, and on no other. An acknowledgement releases mail across the whole network, so an unsigned one is a way to delete a message the attacker never held (section 9.7.1).


9. Relaying and carried messages

A packet may travel further than the radio that sent it. A message to a station no path reaches is handed to a nearby station, which carries it and delivers it on meeting the recipient; a digipeater repeats what it hears so that stations beyond the sender's range receive it.

via: is the list of callsigns that relayed the packet, in order, oldest first. A relay never rewrites f:. It appends itself to via: and leaves the author alone.

t:message f:X1QZ3N d:X1RD89 ts:2026-08-08_14:26:40 q:ack m:meet at the bridge at six
t:message f:X1QZ3N d:X1RD89 ts:2026-08-08_14:26:40 via:X32DVA q:ack m:meet at the bridge at six
t:message f:X1QZ3N d:X1RD89 ts:2026-08-08_14:26:40 via:X32DVA,CT1ABC-9 q:ack m:meet at the bridge at six
t:message f:X1QZ3N d:X1RD89 ts:2026-08-08_14:26:40 via:X32DVA,CT1ABC-9,X3RLY7 q:ack m:meet at the bridge at six

84, 95, 104 and 111 bytes: as sent, then after each of three relays. The recipient reads that the message came from X1QZ3N and travelled through X32DVA, CT1ABC-9 and X3RLY7 in that order.

The hop count is not transmitted. It is the number of callsigns in via:, which every station can count for itself, and a packet with no via: has taken no hops.

The identifier is de9780 in all four. Both the identifier and the signature are computed with via: removed (sections 5 and 7.1), so relaying alters neither, and a station that already holds the message recognises the repeat and does not display it twice.

9.1 How far a packet travels

A relay forwards a packet only while via: holds fewer callsigns than the limit for that packet type:

Packet type Relays
sos, warning 9
everything else 3

The limit belongs to the type rather than to a field. A sender cannot ask the network for more of its airtime than its traffic warrants, and an emergency does not have to remember to ask: sos and warning travel nine relays because they are the packets worth spending a shared channel on, and a chat message travels three because it is not.

9.2 Loops

A station that finds its own callsign in via: does not relay the packet, whatever the count says. The limit bounds how far a packet travels; the path prevents it from travelling in a circle, and neither substitutes for the other.

A relay also drops a packet it has already relayed within the last few minutes, identified by the identifier of section 5. Two digipeaters in range of each other otherwise trade the same packet until the limit is reached, which is legal under the rules above and still a waste of the channel.

9.2.1 When to re-air

The rules above say whether a packet may be relayed. They do not say when, and on a shared bearer that is the difference between one transmission and five.

Every station in range hears the same packet at the same moment, and every one of them that is willing to relay it is ready to transmit in the same instant. On a LAN they collide as duplicates; on a radio they collide as radio.

So a station waits a short random moment before re-airing, and drops its copy if it hears the packet again during that wait. Somebody else was closer to the front of the queue; the packet is already travelling and a second copy adds nothing but noise.

The wait is a fraction of a second on a fast bearer and longer on a slow one, what matters is that it is random, so that two stations do not choose the same instant, and that hearing the packet cancels it.

This works without a new field because a section 5 identifier is computed with sig: and via: removed: the same packet relayed by a different station has the same identifier. "Somebody already said this" is decidable from what is on the air.

9.2.2 When the sender names the relays

Everything above decides who relays from what a station can see. relay: lets the sender say it instead:

t:message f:X1VCVM d:X16JK8 ts:2026-08-28_09:40:00 relay:X3ARK,X3GSLC m:two hops
t:message f:X1VCVM d:X16JK8 ts:2026-08-28_09:40:00 relay:X3ARK,X3GSLC via:X3ARK m:two hops
t:message f:X1VCVM d:X16JK8 ts:2026-08-28_09:40:00 relay:X3ARK,X3GSLC via:X3ARK,X3GSLC m:two hops

80, 90 and 97 bytes: as sent, after the first named station, after the second. The identifier is ccd6ce in all three.

The next hop is the first callsign in relay: that does not appear in via:. Nothing consumes the list and nothing rewrites it; the two fields together say what was asked for and what has happened so far, and the second is what advances. Callsigns compare whole and case-insensitively (section 3.0.1), suffix included, X3ARK-9 is a different device from X3ARK (section 3.1), and a sender that writes one meaning the other has named a station that will never answer.

A station not named does not relay the packet. That is the point: on a shared bearer where everybody hears everybody, section 6.2.1 leaves exactly one relay standing, and which one is a matter of whose random wait was shortest. A sender that needs a particular second hop cannot get one by asking the network more loudly.

A station that is named may still decline. Section 9.12.2 says it of mailboxes and it is just as true here: listing a station is not asking its permission. A named station relays under its own policy and its own airtime budget (section 30), and one that carries nothing is still a good citizen.

Three fields hold a list of callsigns and they are not the same field. relay: is the route asked for, and only the author writes it. via: is the route taken, and every relay appends to it. route: (section 9.10) is the route taken as the recipient attested it, inside a signature. Asked, happened, attested.

relay: is covered by sig: and is inside the identifier of section 5, which via: deliberately is not. So a relay cannot quietly rewrite where a packet was asked to go, and a station must never edit relay:, doing so changes the packet's identity at every hop, and every mechanism that recognises a packet by its identifier stops working at once. It also follows that via: is not a subset of relay: and must never be validated as one: a carrier delivers, a station may relay a packet naming nobody, and an older station relays without reading the field at all.

The section 6.1 budget still binds, counted from via: exactly as before. A relay: naming more stations than the type's budget allows names hops that can never fire, and a sender composing one has made an error worth refusing at the keyboard rather than discovering on the air.

Section 9.2.1's cancel does not apply to a named hop. Under this section at most one station is willing to relay at any moment, so there is nothing for the cancel to prevent; what it would do instead is let a station outside relay: silence the one the sender asked for. The random wait still applies, because it costs nothing and there is no reason for two mechanisms where one will do.

Carrying is not relaying, and section 6.3 is not suspended. A holder may deliver a held packet to the station named in d:, appending itself to via: as it always does, that is the last hop and it is honest. It may not hand the packet to another holder while relay: names hops that have not happened. Mail that cannot move waits, which is what carrying is for.

Naming stations is not naming bearers. A named station relays on whatever bearers it has, exactly as it would for any other packet. Section 12.0's rule about choosing among paths to the same station is unchanged.

A station that does not know this field skips it (section 4) and relays under section 6.2 as before, so relay: narrows the traffic only among stations that understand it. It is a request, not a guarantee, and a sender that needs a guarantee does not have one.

Two costs worth stating. The field is carried on every part of a split message (section 7.6), and with the via: it accumulates a two-hop path adds about thirty-six bytes to a packet that has two hundred and fifty, a sender must leave room for a via: it will never see. And relay: is in the clear beside a sealed x: body: it says nothing about the message and something about who the sender believes can carry it.

Section 9.9 records that each via: which arrives is a route that actually worked. relay: is how a station acts on that knowledge next time.

9.3 Carried messages

A carrier holding a message for a station that is not currently reachable follows the same rules: it appends itself to via: when it finally transmits.

The recipient's s:ack releases carriers still holding a copy. A station that overhears a receipt for a message it is carrying discards its copy, which is why a receipt is worth repeating even after the sender has seen it.


9.4 Carrying toward a place

A message addressed to someone no path reaches, and whom no carrier knows, can still arrive if it says where it is going.

t:message f:X1QZ3N d:X1RD89 ts:2026-08-08_14:26:40 dest:37.98,23.73 near:50km until:2026-09-08_00:00:00 q:sign m:are you still in Athens in September?

150 bytes: Lisbon to Athens, 2852 km, with nothing in between.

Key Meaning here
dest: where the packet is bound
near: how close counts as arrived
until: when it stops being worth carrying
urg: how much it is worth carrying
via: who has carried it, as everywhere else

near: is a separate key from rad: and not a second meaning for it. rad: always describes the area a subject occupies, a fire, a flood, how far a station will travel to deliver. near: always describes how close to dest: is close enough. A warning about a fire being carried to a town needs both at once and they are different numbers (section 9.8).

A carrier accepts a copy only if it expects to reduce the distance to dest:. That is the whole routing rule. A station driving to Madrid is 483 km closer to Athens and takes it; one sailing to the Azores is not and does not. A carrier already in via: does not take it again, and a carrier inside near: stops carrying and starts airing it normally, because it has arrived.

A packet with dest: is not bound by the three-relay limit of section 6.1. Three relays do not cross a continent. It is bound by until: instead, which is why until: is required here and optional everywhere else: a carried packet with no expiry is litter that outlives the reason it was sent.

until: is never more than one year after ts:. A packet still being carried a year later is not in transit, it is lost, and a network that cannot say so accumulates the difference for ever.

There is no copy limit in the format. Each carrier decides what to hold, and store-and-forward.md already bounds that with a per-device quota and eviction by priority, so a limit here would duplicate one the carrier already enforces and cannot be trusted to obey anyway.

9.5 Urgency

urg: takes one of low, normal, high, urgent. It is what a carrier sorts by when its store is full and something has to be dropped.

It is a request, not an instruction. A carrier is free to ignore it, to carry only low traffic for its own reasons, or to distrust a station that marks everything urgent, and stations will. urg: earns its byte because the alternative is a carrier choosing by arrival order, which is worse for everyone.

An sos is not marked urgent; it is an sos, and section 6.1 already gives it nine relays. urg: is for ordinary traffic that happens to matter.

9.6 What a carrier can read

A carried packet passes through strangers. The envelope is what they need and the body is not:

t:message f:X1QZ3N d:X1RD89 ts:2026-08-08_14:26:40 dest:38,24 near:200km urg:high until:2026-09-08_00:00:00 x:<64 characters> sig:<60 characters>

239 bytes. dest:, near:, urg: and until: stay in cleartext because a carrier cannot route without them. m: is replaced by x:, sealed to the recipient's key, so every carrier can decide whether to carry it and none can read it. This is the same rule as section 7.2 and needs no new mechanism.

A sealed body is longer than the text it replaces, so a long carried message is split into parts (section 7.6). Every part repeats the routing keys, since a carrier may hold one part and not another.

dest: tells every carrier roughly where your correspondent is, and d: already told them who. Coarsen it deliberately: dest:38,24 near:200km says "somewhere around Athens" and is 2 decimal places short of a street. Section 15.1 ties decimal places to precision, and here that is a privacy control rather than an accuracy claim.

9.7 Signed receipts

q:sign asks the recipient to sign an acknowledgement. It is a person agreeing that they read something, not a device reporting that bytes arrived:

t:receipt f:X1RD89 d:X1QZ3N ts:2026-08-20_09:12:00 r:766d3e s:sign dest:38.72,-9.14 near:50km until:2026-10-01_00:00:00 sig:<60 characters>

184 bytes. r: names the original message, 766d3e, computed from its sender, timestamp and text, and sig: signs the receipt, which covers f: and r: together (section 7.1). The result is evidence that the holder of X1RD89's key acknowledged that exact message, and it is checkable by anyone holding the public key, not only by the sender.

The receipt carries its own dest: and until:, because it has to hitchhike home the same way the message came.

s: Means Signed
ack it reached a device by default (section 9.7.1)
read it was opened by default (section 9.7.1)
sign a person acknowledged it required

An s:sign receipt without a valid sig: is not a signed receipt. A receiver discards it rather than showing it as one: the state exists because of the signature, and a state that can be claimed without proof is worth less than no state at all.

A signed receipt can be replayed by anyone who heard it, and that is harmless: it names one message and says one thing, so a second copy asserts exactly what the first did.

9.7.1 Receipts without asking

A direct message between two stations that have exchanged one before is acknowledged automatically. Neither operator turns anything on, and the sender does not have to remember q:ack:

t:message f:X1RD89 d:X1A67X ts:2026-08-12_17:28:52 m:on my way
t:receipt f:X1A67X d:X1RD89 r:40f357 s:ack

62 and 42 bytes. r:40f357 is the message's identifier (section 5), so the receipt costs two thirds of what it confirms and names it exactly.

This exists because the alternative is a message that fails in silence. A station that hands a packet to a radio and hears nothing back cannot tell delivery from loss, so it cannot retry, and a lost message is simply gone while both operators believe it arrived. Measured between two phones with no internet: a reply left as a single unacknowledged transmission was recorded as delivered and retried zero times.

When it applies. One condition: a direct message has already passed between the two callsigns, in either direction. From then on each acknowledges the other's direct messages. A relayed message is acknowledged the same way, the receipt travels home as section 9.7 describes, and a carried message is precisely the case where the sender has least other evidence.

When it does not. Each of these would put an acknowledgement on a shared channel for every station that hears it, so none of them is automatic:

Not acknowledged Why
a broadcast (no d:) one packet, every hearer answering
a regional message (dest:, no d:) same, bounded only by the region
a group message every member answering every message
a receipt an acknowledgement of an acknowledgement never terminates
a station never exchanged with see below

q:ack still works, and still asks a station outside these rules for a receipt.

Why a known station rather than everybody. An acknowledgement is airtime paid on a channel everyone shares, and answering a stranger also confirms to anyone listening that this callsign is here and awake. Two stations that have already exchanged a direct message have both of those costs priced in; a stranger has not agreed to either.

An automatic receipt carries no q:, it is a device reporting bytes, not a person agreeing to anything, and that remains s:sign (section 9.7), which is still asked for explicitly. But it is signed, and this is the one place where that is not a preference:

t:receipt f:X1A67X d:X1RD89 r:40f357 s:ack sig:<60 characters>

107 bytes, against 44 unsigned. The reason is what an unsigned one is worth to an attacker. s:ack is not merely a note to the sender: section 8 has every carrier holding that message discard its copy when it hears the matching acknowledgement. So a forged receipt is not a lie about delivery, it is a way to delete a message from the whole mesh, cheaply, without holding anyone's key, for any callsign the attacker cares to name. The victim is told their message arrived, the carriers drop it, and nobody ever finds out.

A receipt whose signature does not verify changes nothing. It does not mark a message delivered, it does not release a held copy, and it does not stop a retry. A station that has never heard the signer's key cannot verify one, and must treat it the same way: unverifiable is not "probably fine". Section 9.7 already says this of s:sign, a state that can be claimed without proof is worth less than no state at all, and the same sentence was always true of s:ack; it simply had not been written down.

On a rated bearer 107 bytes is two and a half times the airtime of 44. The answer is to send fewer receipts, section 9.7.2 already spends them only against evidence the peer is there, and never to send unsigned ones. An acknowledgement nobody can check is airtime spent on nothing.

A gateway acknowledges for a network that cannot. A Meshtastic node that receives a direct message confirms it the Meshtastic way, and nothing it sends can be verified here. The gateway that delivered the message turns that confirmation into a receipt of its own, signed with its own key:

130  t:receipt f:X3DCK0 d:X1QZ3N ts:2026-09-19_12:05:14 r:7108b6 s:ack sig:<60 characters>

f: is the gateway, not the recipient: the receipt says "the other network's device took it from me", and the signature makes the gateway answer for that. It releases a carrier's copy like any verified receipt, because the message has left XPRS and no carrier here can take it further. A refusal is said the same way, s:no with the other network's reason in m:.

9.7.2 When to stop trying

A receipt that never comes is an instruction to stop, not to try harder.

The failure this guards against is the expensive one. A station holding an unacknowledged message for a peer that walked out of range will re-air it on whatever schedule it was given, and every one of those transmissions is spent on a peer that is not there. On a duty-cycled band that is not merely wasteful: section 22.1 counts a retry against the same budget as saying it the first time, so a handful of stations each nursing a few undelivered messages can hold a frequency down between them while delivering nothing.

A retry is spent only against evidence that the peer can be reached. Two kinds count, and either will do:

Evidence Where it comes from
the peer's beacon, heard recently section 15.6, it already names the station and where to write to it
a live route to it the bearer, on the paths that have one

With neither, a station parks the message. Parking is not failure and not delivery: nothing is transmitted, the copy stays held, and the attempt is not counted. The station keeps listening, and when the peer's beacon returns the ladder resumes where it left off rather than having been spent into an empty room. A peer that comes back in an hour is then still owed its message, which is what store-and-forward was for (section 6.3).

This costs nothing to implement, because the evidence is already arriving: a station hears its neighbours' beacons whether or not it has anything to send.

Three more rules keep the acknowledgement itself cheap.

9.8 Delivering to a region

A message with dest: and no d: is addressed to whoever is in that region rather than to a person. Carriers take it there and stations already there air it locally.

t:message f:X3RLY7 ts:2026-08-08_14:26:40 dest:38.72,-9.14 near:30km urg:high until:2026-08-15_00:00:00 m:water is off in the old town until Friday

147 bytes: get this to within 30 km of Lisbon, and it stops mattering on Friday.

Nothing new is needed for this. d: absent already meant "anyone in range" (section 8.1); adding dest: and near: moves which range. The same rules apply: a carrier takes it only if it gets closer, until: is required and is never more than a year out, and urg: decides what survives a full store.

There is no recipient, so nothing is acknowledged. q:sign on a regional message is meaningless and ignored.

Any packet type can be delivered this way, and for a warning the two circles are genuinely different things:

t:warning f:X3RLY7 pos:39.40,-8.20 rad:5km dest:38.72,-9.14 near:40km urg:urgent kind:fire sev:danger until:2026-08-10_00:00:00 ts:2026-08-08_14:26:40

150 bytes: a fire five kilometres across at pos:, to be delivered within 40 km of a town 80 km away. pos: and rad: describe the fire; dest: and near: describe where people need to hear about it. Collapsing those into one key would have made this packet unsayable.

9.9 The same message by two routes

Two copies that travelled differently are still one message:

t:message f:X1QZ3N d:X1RD89 ts:2026-08-08_14:26:40 dest:37.98,23.73 near:50km until:2026-09-08_00:00:00 q:sign via:X32DVA,CT1ABC-9,SV1XYZ m:are you still in Athens in September?
t:message f:X1QZ3N d:X1RD89 ts:2026-08-08_14:26:40 dest:37.98,23.73 near:50km until:2026-09-08_00:00:00 q:sign via:X3RLY7,IT9ABC,SV2QRP m:are you still in Athens in September?

177 and 175 bytes. Both are identifier 766d3e, because an identifier is computed with via: removed (section 5) and that is the only field the two copies differ in.

So the recipient recognises the second copy as one it already holds and shows it once. This is not a rule that had to be added: it falls out of deriving identifiers from the message rather than the journey.

It does answer each copy, with the same receipt. The two cases are indistinguishable on the air, a resend after a lost acknowledgement carries the same identifier as the original, because the identifier describes the message and not the attempt, so a recipient that answered only the first copy would leave a sender whose receipt was lost retrying against a silence it can never break. A receipt is idempotent (section 9.7): re-airing it asserts exactly what the first one did. Bound it at one receipt per message per sender per 20 seconds, so a burst of copies is answered once rather than once each.

The difference between the copies is worth keeping rather than discarding. Each via: is a route that actually worked, which is knowledge no single copy carries: the recipient learns that both SV1XYZ and SV2QRP can reach it, and a reply can be sent back along the one that arrived first.

9.10 Recording the route in the receipt

via: on a carried packet is appended to by each carrier, so on arrival it names everyone who moved it. A signed receipt copies that list into route: and signs it:

t:receipt f:X1RD89 d:X1QZ3N ts:2026-08-20_09:12:00 r:766d3e s:sign route:X32DVA,CT1ABC-9,SV1XYZ dest:38.72,-9.14 near:50km until:2026-10-01_00:00:00 sig:<60 characters>

213 bytes. via: on this packet is the receipt's own journey home, which is a different list and is still being written. route: is the journey the message made, fixed at the moment it arrived.

Because sig: covers everything except itself (section 7.1), the signature binds the signer, the message identifier and the route together. The sender gets back a statement that this person read this message and that it came by these hands, which nobody along the way can alter without breaking the signature.

A carrier that finds itself in a signed route: has evidence it delivered something, which is the only durable record any of this produces.


9.11 How far a packet may go

scope: limits where a packet may be transmitted and repeated. It is optional and the default is global: a packet without it may go anywhere, which is what every packet in this document does.

scope: Meaning
absent, or global anywhere, on any bearer
regional local and regional bearers only: never the internet, never a satellite
local local bearers only: Bluetooth LE, WiFi Direct, WiFi Aware, ESP-NOW and a local network
an ISO 3166-1 alpha-2 code, uppercase not relayed out of that country

Lowercase words and uppercase codes cannot be confused, which is the same convention callsigns and enums already follow everywhere else.

Every bearer has a class, and the three words are the three classes. The classes are defined on the link: vocabulary of section 15.6.1, so that the name a station gives a bearer in an observation is the name that decides what it may carry:

Class link: values What it reaches
local ble, wifi, espnow, lan the room, the building, the network a station is attached to
regional lora, halow, vhf, uhf, hf, cb, pmr as far as a transmitter carries: a valley, a city, a coast
global internet, satellite anywhere

other is global until the station knows better, which is the conservative reading. A bearer belongs to exactly one class, with the one exception section 9.11.1 gives the operator for LoRa. The classes are ordered, local inside regional inside global, and a scope admits its own class and every narrower one: regional may go out on Bluetooth as well as on LoRa, local only on the local class.

A scope binds at both ends of every hop. A station sends a packet only on a bearer whose class the scope admits. And it relays, carries, gates or serves from its archive only a packet it HEARD on a bearer the scope admits: a local packet that arrives over the internet or a satellite has already left its range, and is out of range at every station after that. Such a packet may be read and kept, as any packet may, and goes no further from here; an application does not show it among the traffic of the room its scope names, because it did not come from there. The check is needed because nothing on the wire records the bearers a packet crossed (via: names stations, and is not signed), while scope: is signed and counted in the identifier (sections 5 and 6.1), so the author's word arrives intact and each station can only answer for its own two bearers.

The other direction is not a violation. A global packet that arrived by satellite and is re-aired on Bluetooth is still global: it is shown with the global traffic and may go on to the internet again. The room a packet belongs to is the scope its author wrote, never the bearer that happened to deliver it.

A scope word a receiver does not know is a reason not to relay. Section 4.3 skips a value that does not match its type, and for most fields that is the safe reading. For this one it is not: a skipped scope: reads as global, and global goes to the internet. So a station that finds a lowercase word it does not recognise reads the packet and does not relay, carry or gate it, the same safe direction section 9.11.2 takes for a station that does not know where it is.

9.11.1 local and regional

t:message f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 scope:local m:anyone got a 10 mm spanner?

92 bytes, twelve of them the field.

local names bearers, not a distance. A station must not put the packet on a radio band, on a satellite, or onto the internet. It may put it on Bluetooth, WiFi Direct, WiFi Aware or the network it is attached to.

That is the difference between "the people in this building" and "everyone who can hear a 500 mW transmitter", and it is a privacy control as much as a noise one: a question asked in a marina should not arrive in the next county, and it certainly should not reach a relay that gates to the internet.

LoRa sits on the boundary, and the operator places it. A LoRa link can be a building's own low-power mesh or a forty-kilometre shot across a valley, and no rule written here can know which one a given antenna is. LoRa is regional. Whether a station ALSO treats its LoRa as a local bearer is the operator's setting, and the default is no: a sender marking a packet local is trusting every station within earshot with the promise above, and the conservative reading is the only one a stranger's promise can survive. An operator who knows their LoRa reaches the campsite and nothing beyond may say so, and their station then carries local packets on it. No other bearer moves between classes.

t:message f:X1QZ3N ts:2026-08-08_14:26:40 scope:regional m:road to the bridge is flooded, go round by the mill

regional keeps a packet on the air. It may go on any local or regional bearer, a LoRa mesh across the valley, a HaLow link to the next village, a radio relay on the hill, and never onto the internet or a satellite. It is the neighbourhood that is too big for one building and too small for the world: a road closure, a lost dog, a market moved to Saturday. Section 6.4.1 still binds on licensed spectrum, and regional is no permission to put a self-generated callsign there.

The approved room wires, end to end. These four lines are the whole chat interoperability story, a phone application, a captive-portal web page and a microcontroller's LCD all read and write exactly these, with no translation layer anywhere:

t:message f:X16JK8 ts:2026-08-19_14:37:08 scope:local sig:<60 characters> m:hello from the Chat wapp, locally
t:message f:X1QZ3N ts:2026-08-19_14:51:30 scope:regional m:anyone on the valley mesh tonight?
t:message f:X9WEB ts:2026-08-19_14:58:01 m:round two global
t:message f:X1QZ3N ts:2026-08-19_15:02:10 scope:local root:399227 r:8536fb sig:<60 characters> m:replying inside the local room

The first is the local room: twelve bytes of scope: buy the whole privacy behaviour, everything else is an ordinary message packet. The second is the regional room. The third is the global room, which is simply the default, and a country-scoped message belongs there too. The fourth shows that threads (section 7.4) compose with scope like any other field.

Applications show the three scopes as three conversations. The scope:local traffic is the room of whoever is actually around, the marina, the building, the campsite; scope:regional is everyone within reach of the air; and the unmarked default is the conversation that travels. A chat that mixes them flattens exactly the distinction the field exists to draw. A packet is filed by the scope its author wrote, never by the bearer it arrived on, and one heard out of range (section 9.11) is not filed in any of them. Safety traffic (t:sos, t:warning) is shown whatever its scope. The Regional room is also what a gateway shares with a neighbouring radio network (section 9.11.5): what is said there may be read on that network's public channel, in the clear, and that channel's talk appears here.

A local packet is not carried. Section 9.4 exists to deliver somewhere else later, and somewhere else later is what local excludes. A carrier holding one drops it rather than taking it to another town. A regional packet may be carried, and is handed on only over local and regional bearers: never to an archiver or a carrier reached over the internet.

9.11.2 A country

t:warning f:X3RLY7 pos:39.4012,-8.2043 rad:5km kind:fire sev:danger scope:PT ts:2026-08-08_14:26:40

99 bytes. A station does not relay it out of the named country, which it decides from its own position, the same position that already chose its frequency (section 1 of spectrum.md).

More than one country is a comma-separated list, which a border region needs:

t:warning f:X3RLY7 pos:41.8012,-6.7543 rad:9km kind:fire sev:danger scope:PT,ES ts:2026-08-08_14:26:40

102 bytes: a fire nine kilometres across on the Portuguese side of a border, to be relayed in both countries because the smoke does not stop either.

Radio does not respect borders, and scope: cannot pretend otherwise. A transmission near a frontier is heard across it whatever this field says. What scope: governs is what a station chooses to relay, to carry, and to gate onto another network. Reception is not restricted and cannot be: a station that hears an out-of-scope packet may read it, and simply does not pass it on.

A receiver that does not know where it is treats a country scope as global for reading and refuses to relay, which is the safe direction: it sees the packet and does not spread it.

9.11.3 Gateways

A gateway republishes a packet onto something that is not XPRS: an internet relay, a NOSTR relay, APRS-IS, a web page, a chat room. That is not relaying, the rules in section 6.1 do not reach it, and this is why it has to be said separately.

A gateway treats scope: as binding. It never publishes a local packet at all, never publishes a regional packet onto the internet (a gateway into another radio network takes it there, which is regional, and takes nothing wider: that network's channel is one valley's air, shared with everybody on it, and the conversation that travels the internet does not belong there), and never publishes a country-scoped packet outside that country. A gateway that cannot determine where it is does not publish country-scoped traffic.

This is the leak that matters, because a gateway is the one station whose whole purpose is to move traffic somewhere the sender cannot see.

Three things follow that a community should know before relying on any of it.

The default publishes. No scope: means global, and global includes the internet. A group that does not want its traffic leaving must say so on every packet; silence is not a restriction.

A group is an address, not a boundary. d:LISBOA says where a packet is going, not who may read it. Anyone in range hears it, any station may relay it, and a gateway may publish it. Group membership is not enforced anywhere in this format and cannot be, because a broadcast medium has no door.

That holds for a closed group too (section 10). A member list decides what a client shows, never what may be transmitted or received, so d:X5A3F2 is as public as d:LISBOA, and publishes the roster on top.

Only encryption keeps content private. scope: asks well-behaved stations not to spread a packet. It is a request that a hostile or careless station ignores, and it leaves the text in clear for everyone in radio range regardless. A packet that must not be read by strangers uses x: (section 7.2), and one that must not travel uses both.

9.11.4 Against the other limits

scope: is an additional constraint and replaces nothing. A packet still stops at the relay limit of section 6.1, still expires at until:, and still gets carried only toward dest:. Whichever binds first, binds.

Where scope: and dest: disagree, a country scope with a destination outside it, scope: wins and the packet is not carried. A sender that meant it to travel should not have restricted it.


9.11.5 A gateway from another network

Section 9.11.3 is about packets leaving XPRS. A gateway also brings traffic IN, and then it is the author of a packet it did not write. Seven rules make that honest and keep two gateways from saying the same thing twice.

135  t:message f:MTA1B2C3D4 d:X1QZ3N ts:2026-09-19_12:06:00 scope:regional r:7108b6 zmid:a1b2c3d4bc4c2495 via:X3DCK0 m:thanks, see you there

A reply keeps its thread: the gateway remembers which of the other network's messages it made from which packet, and writes r: accordingly.

What does not cross. A sealed body (section 6.2) cannot: the other network has no key for it, and a gateway that could open it would be reading the mail. It is refused out loud (s:no) to the station that handed it over. And a direct message to an XPRS callsign through a gateway is exactly as private as the other network's public channel, which is to say not at all: the gateway turns it into clear XPRS, and on Meshtastic the key a gateway presents for a callsign is derived from the callsign, so that every gateway can deliver it.

Time crosses as text, when it matters. The other network has no field for when a message was written: a Meshtastic frame carries addresses, an identifier and hop counts, and its reader sees the moment the frame reached their own radio, stamped by their own clock. Three rules follow. A gateway with no clock translates nothing it merely heard into the other network, and nothing in the first moments after it starts: the bearers keep old packets moving (carriers, section 9.4; archives, section 13; a Bluetooth advertisement that repeats), and a gateway that could not tell them from news would hand them to every reader there as said just now. Its own user's words go regardless: a person is waiting, and what they just typed is new. A message older than two minutes when it crosses, carried or relayed from far, says its own time first, [14:26] see you at the quay, in the gateway's local time when it knows its zone (section 11.10) and in UTC with a Z when it does not, with the day in front when it is not the reader's today; two minutes, so a live conversation crosses clean. And old news does not cross at all: half an hour for a broadcast, six hours for a direct message. Coming in, a translation is dated to the minute with the gateway's clock (above), or with its epoch: (section 15.7) when it has none, so that two gateways still compose one packet.

9.12 Where to leave mail for me

t:mailbox names the stations a sender should hand mail to when the recipient cannot be reached directly.

t:mailbox f:X1QZ3N ts:2026-08-08_14:26:40 hold:X3RLY7,X32DVA sig:<60 characters>

125 bytes. hold: lists callsigns in order of preference, and a station that cannot reach X1QZ3N tries X3RLY7 first.

This is the missing half of section 6.4. Carrying toward a place works when the sender knows where the recipient is; a mailbox works when the sender knows who tends to see them. A boat that checks in at the same marina, a person whose neighbour runs a solar node, a group whose members all pass one repeater: those relationships exist and nothing in the format could infer them.

until: bounds the declaration, which matters because the arrangement changes:

t:mailbox f:X1QZ3N ts:2026-08-08_14:26:40 hold:X3RLY7,X32DVA,CT1ABC-9 until:2026-09-08_00:00:00 sig:<60 characters>

160 bytes. A mailbox list with no expiry outlives the friendship, and a sender handing mail to a station that stopped carrying it a year ago gets silence.

A mailbox declaration must be signed, and a receiver that cannot verify one must not act on it.

A declaration is CONSUMED, not only displayed: an archiver must be able to answer "who holds for X1QZ3N" from the declarations it verified (the reverse of the lookup it does for itself), because section 12.8.1 routes held mail by exactly that answer, in hold:'s order of preference.

This is the one packet in the format where forgery pays directly. Anyone who can publish t:mailbox f:X1QZ3N hold:<attacker> collects that station's incoming mail from every polite sender, and the sender believes it delivered. Signing is the default everywhere (section 7.1) and here it is the reason the rule exists: an unsigned mailbox declaration is a request to misroute somebody's mail, and it should be ignored rather than displayed.

9.12.1 Several at once, each for a period

A station publishes as many mailboxes as it has, and they coexist. An earlier draft said the newest declaration replaced the previous one, which made it impossible to say the true thing: that where you are found depends on when.

since: and until: bound each one. A boat that knows its season says so months ahead:

t:mailbox f:X1BOA3 ts:2026-08-08_14:26:40 hold:X3RLY7,X32DVA sig:<60 characters>
t:mailbox f:X1BOA3 ts:2026-08-08_14:26:40 hold:CT1MAR since:2026-09-01_00:00:00 until:2026-09-30_23:59:59 sig:<60 characters>
t:mailbox f:X1BOA3 ts:2026-08-08_14:26:40 hold:EA7CAN,EA7GIB since:2026-11-01_00:00:00 until:2027-03-31_23:59:59 sig:<60 characters>

125, 170 and 177 bytes. Home stations all year, a marina through September, the Canaries from November to March. All three are true and none contradicts another.

A declaration with no since: or until: is open-ended and always applies. One with a window applies only inside it.

Where windows overlap, the narrowest one that contains the moment wins. In September a sender uses CT1MAR rather than the open-ended pair, because a declaration made about September is better information than one made about every month. Within a single declaration, hold: stays in order of preference.

Outside every window a sender falls back to the open-ended declaration, and failing that to any station advertising serve:archive (section 9.2).

9.12.2 Cancelling one

Plans change earlier than they were meant to. A declaration is withdrawn by naming it, exactly as a warning is (section 11.2):

t:mailbox f:X1BOA3 ts:2026-08-20_09:00:00 r:46b4ba remove:mailbox sig:<60 characters>

130 bytes. r:46b4ba is the identifier of the marina declaration and remove:mailbox says what is being withdrawn. The other two are untouched, which is why a cancellation names one instead of replacing all of them.

A cancellation must be signed like the declaration it cancels. An unsigned one is a request to stop delivering somebody's mail, which is an attack rather than an administrative act.

Re-publishing a declaration after cancelling it is allowed and produces a new identifier, because ts: differs. There is no way to un-cancel, and none is needed.

Listing a station is not asking its permission. hold: records where the sender believes their mail will be seen, and a station named in one is free to carry nothing: it is under exactly the quota and priority rules of store-and-forward.md as for any other traffic.

9.12.3 Asking whether there is mail

q:mail asks a station how much mail it holds. only: names whose, and its absence asks about the total the station is carrying for everybody.

t:request f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:26:40 q:mail only:X1QZ3N
t:observation f:X3RLY7 d:X1QZ3N ts:2026-08-08_14:26:41 s:mail mail:3 only:X1QZ3N

The answer is the mail: of section 15.6.5, narrowed: how many messages this station holds for that callsign and would hand over if asked. Zero is answered mail:0 rather than with silence, because "nothing for you" and "did not hear you" are different facts and a station coming back needs to tell them apart.

This exists because the alternative is expensive. Without it the only way to learn whether a station holds anything is cmd:history only:X kind:message (section 11.2), which replays the mail itself, a page of packets, and a place in the asker's hourly budget (section 22.2), to answer a question whose usual answer is nothing. A station returning to a valley asks every archiver in earshot before it asks one of them for contents.

The count is a hint and not a promise, exactly as the beacon's mail: is: what a later cmd:history returns is bounded by the same eviction (section 12.11) and the same budget as everything else. Any asker may be answered, knowing that mail exists is not reading it, and the mail itself is still served only to its two parties.


10. Closed groups

An open group (section 7.3) has no door. Anyone may post to LISBOA, nobody may be removed from it, and a single determined spammer or a persistently abusive participant cannot be dealt with at all. That is fine for a calling channel and useless for a community.

A closed group has a member list, one admin, and moderators. It changes nothing about how packets travel and adds no enforcement anywhere: it lets a group say who belongs, so a client can choose to show only those people.

10.1 A group is a station

A closed group holds a keypair, so it gets a callsign like anything else (section 3):

X5A3F2

The admin is whoever holds the matching private key. That key belongs to the group, not to the person, which is the property everything else in this section rests on: an admin hands the group on by handing over the group's key, and never has to share the private key of their own callsign to do it.

A group announces itself with the packet that already exists for announcing a callsign and a key (section 7.3):

t:identity f:X5A3F2 ts:2026-08-08_14:26:40 k:npub1qz3n7fu9j9uenmyva7ha6x9eqwymytv2847ccv4vxdmn45y50q7h7k5f nick:lisboa-net sig:<60 characters>

187 bytes. It is self-signed, which proves possession of the group's private key (section 7.3), and nick: gives the group a readable name that is shown only when the signature verifies (section 6.3.1). A group needs no announcement packet of its own, no registry and no creation ceremony. It exists once somebody generates a key and says so.

An X5 callsign is a label, exactly as every other callsign is. An attacker who wants a callsign that looks like X5A3F2 can grind keys until one produces those characters, about a million tries at four characters, and about a thousand at two, as the table in section 3 sets out. Section 3 already says this and the answer is the same here: the group is its full public key, and a receiver that has not verified a signature against that key has identified nothing. Two groups wearing the same characters are two groups, and a client that holds the key of one is not fooled by the other.

10.2 Subgroups

A large group wants smaller rooms inside it: a club with a VHF section and a contest section, a marina with one channel per pontoon. A subgroup is not a new kind of thing. It is an ordinary closed group, with its own keypair, its own X5 callsign, its own admin and its own roster, that some other group has listed as part of itself.

Listing one is a grant like any other, and role:sub is what it grants:

138  t:moderate f:X5A3F2 d:X5A3F2 ts:2026-08-08_14:26:40 grant:X5K2M9 role:sub sig:<60 characters>
145  t:moderate f:X5A3F2 d:X5A3F2 ts:2026-08-08_14:26:40 grant:X5K2M9,X5T4WD role:sub sig:<60 characters>
130  t:moderate f:X5A3F2 d:X5A3F2 ts:2026-08-08_14:26:40 revoke:X5K2M9 sig:<60 characters>

Delisting is revoke:, the same packet that removes a person. A client that follows X5A3F2 reads its listings and shows the tree; each subgroup announces its own name with its own t:identity, exactly as its parent does.

Listing confers no authority. This is the rule that keeps subgroups simple, and it follows from the group being its key: X5A3F2 saying that X5K2M9 is part of it does not let X5A3F2 grant, revoke or hide anything inside X5K2M9, because those acts are signed by X5K2M9's key and nothing else will verify. Nor does membership travel down: belonging to a parent is not belonging to a subgroup, and each roster is read on its own.

That is a deliberate difference from moderation systems that walk a tree to decide who may act, and it costs the parent admin nothing in practice: an admin who wants authority over a subgroup creates it and keeps its key, which is the ordinary case. Handing that key to somebody else is how a section gets its own administration, and section 15.6 already describes what handing a group key over means.

Five levels, counting the root. A client ignores a listing that would place a group deeper than that, so a root has at most four generations beneath it. It also ignores a listing that names a group already in its own ancestry, because a cycle is not a tree and two groups listing each other must not become an infinite one.

A listing is a claim by the parent and nothing more. Any group may list any X5 callsign, whether or not that group agreed, and there is no packet to prevent it, the same limit section 3 states about callsigns, for the same reason. What bounds the damage is the rule above: a false listing borrows a name into a menu and confers nothing, and it is visible only to clients already following the group that made the claim.

10.3 Membership

One packet type carries every act of authority. The admin signs as the group:

136  t:moderate f:X5A3F2 d:X5A3F2 ts:2026-08-08_14:26:40 grant:X1RD89,X32DVA sig:<60 characters>
164  t:moderate f:X5A3F2 d:X5A3F2 ts:2026-08-08_14:26:40 grant:X32DVA role:mod until:2027-02-08_00:00:00 sig:<60 characters>
130  t:moderate f:X5A3F2 d:X5A3F2 ts:2026-08-08_14:26:40 revoke:X1PZ4Q sig:<60 characters>

grant: and revoke: take one or more callsigns, so admitting a dozen people is one packet rather than a dozen. role:mod makes a moderator; without it the grant makes an ordinary member. f: is always the signer and d: is always the group the act concerns, so a moderator's act looks the same but is signed by them:

156  t:moderate f:X32DVA d:X5A3F2 ts:2026-08-08_14:26:40 revoke:X1PZ4Q until:2026-08-15_00:00:00 sig:<60 characters>
138  t:moderate f:X32DVA d:X5A3F2 ts:2026-08-08_14:26:40 r:89a9c8 hide:message sig:<60 characters>

A suspension is a revocation with an end. revoke: alone removes somebody; revoke: with until: removes them until that moment and no longer. until: keeps the meaning it has everywhere else in this document, when the sender expects the condition to end, and revoke: keeps the meaning it has with or without it.

A moderator may revoke and hide. Only the admin may appoint. Two tiers is the whole hierarchy. hide:message asks clients not to display the packet named in r:; it cannot unsend anything, because nothing on a radio can.

There is no application packet. Asking to join is an ordinary message to the group, which needs no new type and leaves no permanent signed record that a person asked and was refused.

10.3.1 Nobody is a member without saying so

A grant is an offer. It confers nothing until the person named signs an acceptance, and until then they are not a member, are not shown as one, and may not post as one.

The reason is section 15.7: the roster is public, permanent and non-repudiable. Without this rule an admin could put anybody into that record, a competitor, a stranger, somebody they wish to embarrass, and the person named would have no act anywhere in the log to answer it with. Consent that leaves no trace is not consent.

The member signs as themselves, so f: is the member and d: is still the group. It is the same shape a moderator's act already has, and it needs no new packet type:

139  t:moderate f:X1RD89 d:X5A3F2 ts:2026-08-08_14:26:40 r:9f2c1a accept:member sig:<60 characters>
136  t:moderate f:X32DVA d:X5A3F2 ts:2026-08-08_14:26:40 r:4b81e7 accept:mod sig:<60 characters>
128  t:moderate f:X1RD89 d:X5A3F2 ts:2026-08-08_14:26:40 leave:group sig:<60 characters>

r: names the grant being accepted, which is what makes the acceptance evidence rather than a floating assertion: it says this offer, at this moment, and a grant that was later withdrawn cannot be accepted after the fact. It is the use r: already has in r:<id> hide:message.

leave:group is the other half, and it exists for the same reason. Leaving is the member's to decide and needs nobody's agreement, so it takes no r:. What it leaves behind is the point: a signed record that the person went, rather than a silence an admin could explain any way they liked.

A role:sub listing needs no acceptance. A subgroup is a group, not a person; section 15.2 already says a listing "is a claim by the parent and nothing more" and confers nothing. Asking a keypair to consent would be ceremony without meaning.

A revoke: needs no acceptance either. Removal is the group's to decide. Only joining requires agreement from both sides, because only joining puts a person's name in somebody else's record.

10.4 Reading the log

Every act is signed and they accumulate; a client replays what it has heard. Three rules decide what the result is, and they exist so that two implementations reach the same answer from the same packets.

Authority is judged at the moment of the act. A moderator's revoke: counts if that callsign held role:mod at the act's ts:, whatever their status now. Otherwise removing a moderator would either silently undo a year of legitimate moderation, or leave an abusive one's suspensions standing for ever.

Newest wins, per signer. Where one signer contradicts themselves, the later ts: stands; where two acts share a ts:, the smaller identifier (section 5) stands, so a tie is broken the same way everywhere. A ts: more than a few minutes in the future is discarded, or a rogue moderator would win every disagreement for ever by dating a packet to 2030. An until: more than a year past its own ts: is discarded for the same reason.

The admin can void a moderator's record. revoke: with since: withdraws the moderator and everything they did from that moment:

156  t:moderate f:X5A3F2 d:X5A3F2 ts:2026-08-08_14:26:40 revoke:X32DVA since:2026-08-01_00:00:00 sig:<60 characters>

Without it, a compromised moderator key that suspends fifty people costs fifty packets to undo, each one a packet that is unsafe to lose.

A grant naming a person is pending until its acceptance is heard. Membership begins at the acceptance's ts:, and only if the grant was in force at that moment, a grant already revoked cannot be accepted afterwards. A pending grant is not membership: it is not shown as one and it confers no right to post. A client that holds the grant but not the acceptance therefore reports the person as invited, not as absent, because those are different facts and only one of them is the person's doing.

Consent does not carry across a departure. After leave:group, a later grant needs a new acceptance. The alternative would let an admin re-add somebody who left by replaying an old offer, which is the harm section 10.3.1 exists to prevent, arriving by another door.

Removal needs no acceptance, so a revoke: takes effect at its own ts: under the rules above and nothing waits on the person removed.

Note what is not inherited from section 9.12.1: a mailbox declaration is chosen by the narrowest window containing the moment, and membership is not. Narrowest-window-wins would demote a moderator the instant any narrower grant existed.

10.5 Expiry, and what a quiet group does

until: on a grant is optional, exactly as it is on a mailbox declaration (section 9.12.1): a grant without one is open-ended and stays in force. A revocation is kept indefinitely, because a client that forgets one and then hears a replay of the grant it cancelled would readmit somebody who was removed.

The asymmetry is deliberate and worth stating the other way round: losing a grant is safe and losing a revocation is not, so the format keeps revocations and lets grants stand.

until: on a moderator's grant is the closest thing to cleanup this section offers, and it is enough. A moderator who stops operating falls off when their grant expires, with no vote, no timer and no act by anybody.

10.6 When the admin is gone

Succession is handing over the key. The outgoing admin gives the group's private key to whoever takes it on; the callsign, the roster and every past grant stay valid, because the root of trust has not moved. There is no heir packet and no inactivity rule.

There is deliberately no mechanism that infers an absent admin from silence. A healthy group with no membership churn emits no admin packets at all, so any such timer fires on an admin who is present and simply had nothing to sign. Section 29.5 already refuses this inference for challenges, "treating silence as guilt would make the network hostile to the small stations it exists for", and it is worse here, because clients that heard different packets would promote different successors and split the group permanently, with no way for either side to notice.

Three costs come with key handover, and none of them has a protocol answer:

The remedy in every one of those cases is the same, and it is social rather than technical: found a new group and move to it. That is what a community does anyway when its administration fails, it needs no packet, and unlike an automatic succession it cannot silently fork a group in two.

10.7 What a client shows

Membership decides display and nothing else. A closed group is not a private one, and three rules keep the difference honest.

Safety traffic is never filtered. t:sos, t:warning, t:info and direct replies to them are shown whatever the roster says. A member whose grant a receiver never heard must not have their call for help hidden by their own group; section 23 makes the same argument about encryption, and it applies here with more force, because a missing grant is an accident rather than a choice.

A client that cannot verify fails open, and says so. Without the group's key, or knowing its own grant set is incomplete, it shows everything and marks the group unverified. A closed group whose announcements have not arrived must look broken rather than empty, since the alternative is a silent, invisible failure that looks exactly like nobody talking.

The roster is public, permanent and non-repudiable. Grants have to reach strangers for anyone to bootstrap, so they cannot be sealed with x: or held back with scope:local. What a closed group publishes is a signed list of everyone who belongs, including members who never speak, and a complete history of who suspended whom and when, gatewayed to the internet like anything else. That is more exposure than an open group, where only the people who talk are visible. A group that needs its membership kept secret cannot have it this way. x: conceals what is said and nothing conceals who belongs, because the roster is what a stranger must read in order to honour it at all.

None of this contradicts section 9.11.3. A group is still an address and not a boundary: anyone can still transmit to X5A3F2 and everyone in range still hears it. Design rule 6 also stands, every packet remains fully readable with no prior state, and the roster changes only what a client chooses to show.

10.8 Bootstrap, and not becoming a weapon

Any member may rebroadcast the grants it holds. They are signed by the group, so a newcomer verifies them against the group's key and needs to trust the rebroadcaster not at all.

Two limits keep that from being an amplifier, both following section 29.4:

A rebroadcast is a relay and not a new origination, so it stays under the limit of section 6.1 and does not earn a fresh three hops by being re-signed.

10.9 Propagation and discovery

Sections 10.1 to 10.8 say what a group is and who may act in it. They do not say how a group's traffic travels or how a station finds one, and without that a group works only among stations already in earshot of each other.

Nothing here is a new bearer or a new sync protocol. A group is a station, its acts are ordinary signed packets, and they spread the way every other packet does. What follows is only what a station may say about the groups it keeps, and what it may promise.

10.9.1 Hosting is a directory line

A station that keeps a group's traffic says so the way section 12.9 already says everything of this kind: the group's X5 callsign appears as a line in the archiver's XDIR1 directory, and the directory is named by file: on its signed t:service.

There is no new field, and deliberately nothing on the beacon. An X5 callsign is a callsign (section 15.1), so a directory line naming a group is already legal and already means what it needs to mean. A beacon is the wrong place twice over: section 22.1 asks that service announcements go out "on a period measured in tens of minutes rather than seconds", and a list that grows with the number of groups kept has no bounded size to fit beside hears:.

A hosting line is a claim about capability, not about completeness. It means ask me about this group. It does not say how far back the station holds, and section 30.3 already forbids saying so: "the claim is 'ask me', never 'I hold everything since a date'". A station that keeps one week of a group and a station that keeps five years publish the same line.

10.9.2 Depositing a group's traffic

Any member may hand a group's packets to an archiver, and to a always-on archiver in particular. This is section 15.8's rebroadcast with a destination: the packets are signed by the group and by their authors, so the archiver trusts the depositor for nothing and verifies everything.

Two limits, both already stated elsewhere and both binding here:

10.9.3 What an archiver keeps

An archiver may bound what it keeps for any one group: a byte ceiling per X5 callsign, and within that ceiling the oldest goes first.

Section 12.11 already orders eviction by class and then by age, over three classes, the spool, undeclared custody mail, and declared mail. A group is none of those. Its traffic is addressed to an X5 rather than to a person, and no hold: declaration governs it, so under 12.11 alone a group's history is evicted by global age: one loud group crowds out every quiet one, and a small group that speaks once a month loses its whole record to a busy month elsewhere. A per-group ceiling is the smallest rule that prevents this, and it is a fourth axis on 12.11 rather than a change to the three it has.

This changes nothing a client may assume. Section 30.3 already says a client "must never assume any depth exists", so a group's history being bounded, and bounded differently at every archiver, is the condition clients are already required to tolerate. An archiver that evicts says nothing about it: there is no packet announcing what was dropped, because a station that could hear such an announcement could equally have asked.


11. Commands

t:command asks another station to do something. t:result says what happened.

t:command f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:26:40 cmd:door-open arg:north sig:<60 characters>

139 bytes. cmd: is a label naming the action and arg: carries its arguments, comma-separated. What the words mean is agreed between the two stations and is not this document's business.

This is not t:request. That asks for state from a closed vocabulary, send me your position, send me your battery, and reports. A command acts, its vocabulary is whatever the operator defines, and reporting a battery level is not the same act as unlocking a door.

11.1 The reply, immediately and again later

A station answers a command at once, even when it cannot finish it.

t:result f:X3RLY7 d:X1QZ3N ts:2026-08-08_14:26:40 r:747ae8 code:202 sig:<60 characters>
t:result f:X3RLY7 d:X1QZ3N ts:2026-08-08_14:29:12 r:747ae8 code:200 sig:<60 characters>

132 bytes each: accepted at 14:26:40, done at 14:29:12. code:202 says the command arrived and is being worked on; code:200 says it finished. A sender that hears nothing knows the command did not arrive, which is what makes answering before the work is done.

Any number of results may name one command, and a late one needs no new mechanism: r:747ae8 is the command's derived identifier (section 5), and it is the same however many minutes pass.

code: Meaning
200 done
202 accepted, working on it
206 part of the answer, more on request (section 11.2.1)
400 understood, arguments wrong
403 refused, not permitted
404 unknown command, or nothing held to answer it
408 too old, outside its freshness window
429 over budget, ask later or ask elsewhere (section 30)
500 tried and failed
t:result f:X3RLY7 d:X1QZ3N ts:2026-08-08_14:26:40 r:747ae8 code:403 sig:<60 characters> m:not on the allow list

156 bytes. m: is detail for a person reading a log.

Numbers sit oddly beside sev:danger and kind:fire, and are still right here. The outcome space is open-ended in a way a word list is not, and these particular numbers are understood by everyone who has ever written a web client.

11.2 The commands this document defines

What a command word means is agreed between two stations and is mostly not this document's business. Two are the exception, because they cannot work between strangers if every station names them differently.

cmd:history asks a station to re-air what it kept. It is how somebody back from four days at sea catches up on a townhall that was aired once while they were away.

153  t:command f:X1BOA3 d:X3RLY7 ts:2026-08-08_14:26:40 cmd:history since:2026-08-04_00:00:00 sig:<60 characters>
165  t:command f:X1BOA3 d:X3RLY7 ts:2026-08-08_14:26:40 cmd:history since:2026-08-04_00:00:00 only:X5A3F2 sig:<60 characters>

since: and until: bound the window and already mean exactly this everywhere else. only: narrows the replay to one callsign or one group, which on a slow bearer is the difference between a useful answer and an unusable one.

kind: narrows it to the packet types it names, one, or a comma-separated list, each named by the t: value it matches:

166  t:command f:X1BOA3 d:X3RLY7 ts:2026-08-08_14:26:40 cmd:history since:2026-08-04_00:00:00 kind:message sig:<60 characters>

only: and kind: are different questions and neither substitutes for the other. only: asks whose traffic; kind: asks what kind. An archiver keeps everything it hears, and on a channel where presence beacons outnumber conversation, which is every channel, a page of the newest twelve packets is twelve beacons. Without kind: the asker has no way to say it wanted the talking. An implementation that answers only: by matching a TYPE name has merged the two, and then only:X5A3F2 matches nothing while only:message appears to work: the bug looks like a feature until the day somebody asks the question only: is actually for. Absent, kind: matches every type.

A standard command carries its parameters in the keys the format already has, not in arg:. arg: is positional, and design rule 1 says there are no positional fields; it stays for operator commands, where this document has no key to offer.

cmd:update asks a station to install a firmware version. It is the third that cannot work between strangers if every station names it differently, and it is specified in section 11.8 because what it costs, a station that reboots into code somebody else chose, deserves its own rules rather than a line in a table.

cmd:file asks for the bytes behind a file: reference.

177  t:command f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:26:40 cmd:file file:nYxKzGm4vT2pQ8dW5jR7cL0aFbNs9hUe3oXiC6EkM1w.jpg sig:<60 characters>
186  t:command f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:26:40 cmd:file file:nYxKzGm4vT2pQ8dW5jR7cL0aFbNs9hUe3oXiC6EkM1w.jpg off:64kB sig:<60 characters>

Until now a file: reference could be shown and not resolved, which made every photograph in the format decoration. The command says what is wanted and never how it should travel; a station advertising serve:files (section 13) answers, and which bearer carries the bytes is the transport's business and not this document's.

off: resumes: send from that byte offset, because the first attempt died at 64 kB and the 64 kB that arrived are verified against the piece list (7.7.2) or simply kept. A station that cannot resume ignores off: and sends from zero, the requester receives some bytes twice, and the file still verifies or still fails as a whole.

The reply flow gives cmd:file's codes their concrete meaning. 202, I hold it and it fits my budget; the bytes then move on whatever bulk lane the pair's bearers offer (a GATT session beside the advert channel, a Reticulum resource, a fetch across the LAN, examples, not requirements), and 200 follows only after the REQUESTER's own hash check passed, making the final receipt a statement about content rather than about transmission. 500, the transfer started and died. 404, not held. 403, refused, and a file too large for the station's budget is refused here with m: saying so, which is what size: on the description exists to prevent. 429, over budget this hour, with alternates in m: when the station knows any (section 22.2).

The hash is public, the bytes are not. A file: reference may be shared, indexed and asked after (q:have) freely, so anyone may learn who holds a file. Serving the bytes is a separate decision. A cmd:file MUST carry the requester's f: and a sig:, and a holder serves a PRIVATE file only to a caller whose signature verifies as a callsign in the file's audience, the audience of the message that shared it. A public post reaches everyone, so its file is served to anyone; a closed group's file is served to a current member, the membership that already gates posting (section 26.7); a 1:1 message's file is served to either participant. An unsigned or unauthorised ask for a private file is refused 403, exactly as a too-large one is, so the caller does not retry. A file's reach is thus the reach of the words that carried it: a picture posted to a group is no more fetchable than a message posted to it, and an unbound bare hash (a pinned artifact, a public release) is open by default.

cmd:put is the same exchange in reverse: I hold these bytes, take them.

211  t:command f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:26:40 cmd:put file:nYxKzGm4vT2pQ8dW5jR7cL0aFbNs9hUe3oXiC6EkM1w.jpg size:2MB until:2026-09-08_00:00:00 sig:<60 characters>
132  t:result f:X3RLY7 d:X1QZ3N ts:2026-08-08_14:29:02 r:3148dd code:200 sig:<60 characters>

The ask names the file and its cost up front, size: is mandatory here, because accepting bytes unseen is how a small station is filled by a stranger, and until: bounds the stay, the discipline of section 12.7: a deposit is a hold, not an archive. 202 means bring it, and the bytes travel as a cmd:file answer does, in the other direction. The receiver verifies what arrived against file: and answers the signed 200 above: a custody receipt for bytes, section 9.7's receipt discipline applied to a file. A station already holding the file answers 200 immediately, the bytes exist, custody is real, and nothing needs to travel; a sender must treat that as success, not as an error. 403 and 429 refuse as they always do.

Why deposit at all: the recipient is away, and the file's author will be too. A photograph left with the mailbox station a t:mailbox (section 9.12) names, or with any station advertising serve:files, is a file that arrives next week without either party being awake at the same time, store and forward for content, under the same quota rules as everything else a station carries.

11.2.1 What comes back

The answer is the ordinary sequence of section 11.1, accepted, then done:

132  t:result f:X3RLY7 d:X1BOA3 ts:2026-08-08_14:26:41 r:747ae8 code:202 sig:<60 characters>
132  t:result f:X3RLY7 d:X1BOA3 ts:2026-08-08_14:31:02 r:747ae8 code:200 sig:<60 characters>

Between them the station re-airs the packets themselves. code:404 says nothing was held for that window, code:403 that the station will not serve this requester, and code:429 that it is over budget, with, by section 30, the names of stations that might serve instead.

The replay is the original packets, unchanged. f:, ts: and sig: are exactly as first transmitted, so authorship survives having been held for days by a station nobody trusts. It cannot alter what it replays without breaking a signature, and it cannot invent traffic that was never sent.

A station answers with as much as it can afford, and says there is more. A week of a busy group will not fit in one exchange on a bearer that owes several seconds of silence per packet, and a station must not have to choose between sending everything and sending nothing.

132  t:result f:X3RLY7 d:X1BOA3 ts:2026-08-08_14:31:02 r:747ae8 code:206 sig:<60 characters>

code:206 closes a page rather than the request: what came before it is a complete, verifiable part of the answer, and more exists. code:200 in the same place means that was all of it.

A page is continued by asking again for a narrower window, not by a cursor:

179  t:command f:X1BOA3 d:X3RLY7 ts:2026-08-08_14:33:10 cmd:history since:2026-08-04_00:00:00 until:2026-08-06_11:02:44 sig:<60 characters>

A replay runs newest first, so the requester always knows where the page stopped: it moves until: to the ts: of the oldest packet it received and asks again. Newest first is also the right order for a person, somebody back from four days at sea wants last night before last Tuesday, and a page that never arrives costs them the least.

Nothing here is stateful. The station keeps no cursor, remembers no session and owes the requester nothing between exchanges, so a request that is never continued costs it nothing, and a station that reboots mid-backfill has broken no promise. Repeating a boundary packet is free for the same reason everything else here is: duplicates collapse on their identifiers.

Derived identifiers make backfill safe by construction, and this is the part worth understanding before implementing any of it. A replayed packet has the same identifier it always had (section 5), so a client that already holds it recognises the duplicate and keeps one copy. That single property removes the machinery every comparable protocol needs: no cursors to persist, no sequence numbers to allocate, no agreement about where one station's history ends and another's begins, and no bug at the boundary between two windows. Asking two stations for overlapping windows costs airtime and nothing else.

A station that keeps a spool says so with serve:archive (section 13). What it keeps, for how long and for whom is its own to decide and to change: section 30.3 says why this document sets no retention period, and section 22.2 what a station owes a stranger regardless.

11.2.2 A file transfer, on the wire

Illustrative, not normative: the packets below are this document's, the binary frames between them belong to the bearer (here BLE's session protocol, docs/mesh.md) and are shown ONCE so the division of labour is visible, XPRS asks and receipts, the bulk lane moves bytes, and neither does the other's job. X1QZ3N deposits a 240 kB photograph with X1RD89:

advert channel (XPRS) -----------------------------------------------
X1QZ3N-> t:command f:X1QZ3N d:X1RD89 ts:2026-08-17_12:00:00 cmd:put file:nYxKzGm4vT2pQ8dW5jR7cL0aFbNs9hUe3oXiC6EkM1w.jpg size:240kB sig:<60 characters>
X1RD89-> t:result f:X1RD89 d:X1QZ3N ts:2026-08-17_12:00:03 r:a91f04 code:202 sig:<60 characters>

-- bulk lane (binary, one ATT write per frame; 4D 01 = the session magic),
X1QZ3N-> 4D 01 20  FILE_OFFER   xfer=7, sha256 (32 bytes), size=240640,
                               ttl, origin "X1QZ3N", target "X1RD89",
                               ext "jpg", name "photo.jpg"
X1RD89-> 4D 01 21  FILE_ACCEPT  xfer=7, offset=0, window=16
X1QZ3N-> 4D 01 23  CHUNK        xfer=7, offset=0,   498 raw file bytes
X1QZ3N-> 4D 01 23  CHUNK        xfer=7, offset=498, 498 raw file bytes
        ...16 chunks per credit window...
X1RD89-> 4D 01 24  WIN_ACK      xfer=7, next=7968, window=16
        ...until offset reaches 240640...
X1QZ3N-> 4D 01 25  FILE_DONE
X1RD89-> 4D 01 26  FILE_OK      (receiver's own sha256 matched file:)

-- advert channel again ------------------------------------------------
X1RD89-> t:result f:X1RD89 d:X1QZ3N ts:2026-08-17_12:02:31 r:a91f04 code:200 sig:<60 characters>

The portion of the file on the air is the CHUNK frame: a 3-byte envelope, the transfer id, the byte offset, then raw file bytes, unencoded, the packet grammar never touches them. A lost chunk costs one window, because the receiver's WIN_ACK names the next contiguous offset it wants and the sender rewinds to it. The final signed code:200 is the only durable record: a custody receipt naming the command, issued only after the receiver hashed what it holds. accept at offset == size is how "I already have it" is said without moving a byte.

The same two XPRS packets bracket the transfer whatever the bearer, a Reticulum resource, a LAN fetch, a swarm, with only the middle block changing, which is why it stays out of this document. When the lane is not obvious from where the pair already is, one of them proposes it with a working-channel invitation (section 14.7) and the other agrees or declines before a byte moves.

11.3 Keeping it out of the conversation

Four rules, each closing a specific confusion.

t:command and t:result are distinct packet types, so a station filters them on the first field without parsing anything else.

Neither is ever rendered as a message. A command is not chat, and it must not appear in a conversation view even when it carries m:.

Neither is replied to or reacted to (section 7.5). They are protocol machinery like a receipt or a challenge.

m: is detail, never the command. A bot reads cmd: and arg:; the free text is for the operator afterwards. A station that parsed instructions out of m: would have built a natural-language interface to its front door.

11.4 Security

A packet that opens a door is the highest-value forgery in this format, and the rules here are stricter than elsewhere because of it.

A command must be signed, and one that cannot be verified is discarded. Signing is the default everywhere (section 7.1); here it is a requirement, and "unsigned" is not a state a command may be acted on in.

A command expires. Without that, one signed packet opens a door for ever to anyone who recorded it. The default window is 300 seconds from ts:, and until: extends it deliberately. Outside the window the answer is code:408.

Five minutes rather than the sixty seconds section 29.4 gives a challenge, because a challenge is a direct exchange between two stations and a command crosses real bearers: LoRa at SF9 owes 5.5 seconds of silence for every packet under a 10 percent duty cycle, and a relay hop or two on top can honestly take longer than a minute. Five minutes is still far too short for a recording to be useful hours later.

Commands are never carried. Section 9.4 exists to deliver somewhere else later, and later is precisely what a command must not be. A carrier drops one rather than parking it.

Repeating a command does not repeat the action. The derived identifier of a retransmitted command is unchanged, so a station that has acted on 747ae8 recognises the second copy and answers again without opening the door twice. Idempotency falls out of section 5 rather than needing a rule.

Authentication is not authorisation, and this format only provides the first. A signature proves which callsign sent a command. Whether that callsign may open that door is an allow-list held by the station acting on it. Section 11.9 is how the owner puts that list on the wire; checking it is still the station's alone, and nothing in XPRS checks one for it. A bot that acts on any correctly signed command has an open door with extra steps.

Where bystanders should not learn what is being operated, seal it:

t:command f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:26:40 x:<64 characters> sig:<60 characters>

182 bytes, sealed and signed. t:, f:, d: and ts: stay in clear so the packet can be routed and its freshness checked without reading it.

What a sealed command hides is its fields, one per line. The plaintext is key:value lines, the first of them cmd:, and a value runs to the end of its line, spaces included: a network name or a password travels as it is, which packet grammar, where only m: may hold a space, could not carry. A station checks the signature and the allow-list before it decrypts anything, so only somebody entitled to command it can make it try, and a plaintext that does not begin with cmd: is answered code:400.

11.5 Commands too long for one packet

A command splits across parts exactly as a message does (section 7.6), which matters for the case that motivates it: a spoken instruction, transcribed, and handed to a station that interprets it.

cmd:interpret says the text in m: is the instruction and is to be read rather than matched:

t:command f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:26:40 cmd:interpret n:1/3 m:open the north door for thirty seconds then switch the yard light on
t:command f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:26:40 cmd:interpret n:2/3 m:and leave it until sunrise, and if the water tank is below a quarter
t:command f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:26:40 cmd:interpret n:3/3 sig:<60 characters> m:start the pump for ten minutes and tell me what the level was

141, 141 and 199 bytes. Reassembled it is 266 bytes, which is why it split.

This is the one command whose payload is m:, and it is an explicit opt-in rather than a hole in section 11.3. Everywhere else a bot reads cmd: and arg: and never takes instructions from free text. A station that does not interpret natural language answers 404, and one that does has said so by accepting this command word.

The reply names the whole command:

t:result f:X3RLY7 d:X1QZ3N ts:2026-08-08_14:26:40 r:863d7f code:202 sig:<60 characters>

132 bytes. r:863d7f is the identifier of the reassembled packet, not of any part. Each part has an identifier of its own and none of them is the command's.

Three rules on top of section 7.6, all of them about not acting too early.

A station acts only on a complete, verified set. The signature is on the last part and covers the reassembled packet, so a partial set proves nothing about who sent it. Half a command is not a smaller command.

Parts are held for the command's freshness window, not the ten minutes a message gets. A set still incomplete at 300 seconds is discarded, because a command that has expired is not worth assembling.

The window runs from ts:, which every part shares. A three-part command is one command that took a few seconds to arrive, not three events.

11.6 Interpretation is not authorisation either

cmd:interpret puts attacker-influenced text in front of a language model, and that is worth stating plainly rather than discovering later.

A signature proves which callsign sent the text. It says nothing about whether the text is a good idea, and a model asked to interpret "open the north door" will do exactly as well with a sentence designed to talk it into something else. The allow-list of section 11.4 matters more here than anywhere in this document, because it is the only thing standing between a stranger and the interpreter.

A model's output should not be the last check before a physical action. Map what it produces onto the same fixed set of commands a cmd: would have named, and apply the same permission test. An interpreter that can emit any action at all has made the allow-list decorative.

11.7 Devices: a shared vocabulary for acting on things

A command word is agreed between two stations (section 11), and between two stations that is enough: cmd:door-open works because both ends chose it. It stops working the moment the devices belong to strangers, a lamp cannot be operated by a visiting phone that has never heard the lamp-owner's word for "on". The home-automation industry spent a decade arriving at this lesson, and its answer (Matter is the current name for it) is not a wire format worth importing but a vocabulary worth compressing: devices interoperate when the words for acting on them are few, fixed and shared. This section is that vocabulary in XPRS grammar.

cmd:set asks a device to make a state true, and carries its parameters in the keys of this document, never in arg: (the rule of section 11.2):

132  t:command f:X1A67X d:X1LAMP ts:2026-08-19_18:30:00 cmd:set state:on sig:<60 characters>
142  t:command f:X1A67X d:X1LAMP ts:2026-08-19_18:30:00 cmd:set state:on level:40% sig:<60 characters>
136  t:command f:X1A67X d:X1DOOR ts:2026-08-19_18:30:00 cmd:set state:locked sig:<60 characters>
134  t:command f:X1A67X d:X1HEAT ts:2026-08-19_18:30:00 cmd:set target:21C sig:<60 characters>

Three keys carry the whole model:

Key Type Meaning
state enum the device's principal condition, from the closed list below
level qty, proportion how far, when a condition is partial: a dimmer, a valve, a volume
target qty the setpoint a device holds a reading at, in the reading's own unit

state: takes a word from a closed list this document owns:

Word Device class May be commanded
on, off switch, light, pump, relay yes
open, closed contact, valve, gate, cover yes
locked, unlocked lock yes
motion, clear occupancy sensor no, report only
pressed button, doorbell no, report only

Closed, exactly because of who must agree on it: two strangers' devices, made in different years by people who never met. An operator inventing a word here would be back at cmd:door-open. A device asked for a state it does not have, state:disco, or state:pressed, which is a thing that happens and not a thing that is made true, answers code:400. New words need this document to change, which is the cost of the only list everybody holds.

level: accompanies a state rather than implying one: cmd:set state:on level:40% says both, and a report says both back. target: is the setpoint counterpart of a measurement key, temp: (section 15.3) reports what IS, target: what the device is asked to hold:

76   t:observation f:X1HEAT temp:19.5C target:21C batt:64% ts:2026-08-19_18:30:00

The result states what IS, not what was asked. The keys of the command come back carrying the condition the device actually reached, and the code: table of section 11.1 needs nothing added:

151  t:result f:X1LAMP d:X1A67X ts:2026-08-19_18:30:05 r:1cc8af code:200 state:on level:40% sig:<60 characters>

Reading a device is the existing request, with one new word (section 8):

58   t:request f:X1A67X d:X1DOOR ts:2026-08-19_18:30:00 q:state
84   t:observation f:X1DOOR d:X1A67X state:locked batt:82% ts:2026-08-19_18:30:00 s:state

And a sensor that has something to say just observes, unsolicited, the way every observation in section 15 already travels, a doorbell press is state:pressed with the owner in d::

44   t:observation f:X1DOOR state:closed batt:82%
68   t:observation f:X1BELL d:X1A67X state:pressed ts:2026-08-19_18:30:00

Section 11.4 is not optional here, and a lock is the reason it is written the way it is. The network is open; anyone in radio range can air a packet. So: a cmd:set that is unsigned or fails verification is discarded, never answered. A verified signer must still be on the allow-list the device's owner holds, or the answer is code:403:

138  t:command f:X1RD89 d:X1DOOR ts:2026-08-19_18:30:00 cmd:set state:unlocked sig:<60 characters>
156  t:result f:X1DOOR d:X1RD89 ts:2026-08-19_18:30:05 r:498c72 code:403 sig:<60 characters> m:not on the allow list

The 300-second window and the derived-identifier idempotency of section 11.4 are what keep a recorded state:unlocked from opening the door tomorrow, and carriers drop commands rather than park them (section 11.4), because later is what an act must never be. A report is different: state:pressed from an unsigned doorbell is a claim like any observation, weighed like one (section 15.5), and acted on by a person rather than a mechanism.

11.7.1 Controllers, and the X4 callsign

A pump has no radio. What it has is a controller, an ESP32 on the wall, a node in the shed, that switches it, reads it, and speaks XPRS on its behalf. The controller pattern is one radio operating several devices, and the format gives each operated device a callsign of its own: X4 (section 3), derived from a keypair generated for that device, whose private half the controller holds. On the air the controller is invisible: a command is addressed d: to the device, and the answer comes back f: the device, signed with the device's key, the controller signing as its proxy, which it can do for the same reason it can switch the pump.

The tempting shortcut is suffixes of the controller's own callsign, X3RLY7-1, X3RLY7-2, and it is wrong for the reason section 3.1 makes plain: a suffix shares the callsign's key. Every device would sign with the controller's key, so an allow-list could not name one device without naming them all, and no device could ever be revoked, rotated or handed to a new owner on its own. A keypair per device is what makes each one separately addressable, separately allow-listed, and separately disownable: selling the pump is handing over its private key, and a prudent buyer rotates it, a new key, and with it a new callsign, because a key that changed hands is not the key it was.

A controller says it operates devices, and each device then speaks for itself, an identity binding its callsign to its key, and observations as any station airs them:

126  t:service f:X3RLY7 serve:relay,devices ts:2026-08-19_18:40:00 sig:<60 characters>
186  t:identity f:X4PL3M ts:2026-08-19_18:40:00 k:npub1pl3m7fu9j9uenmyva7ha6x9eqwymytv2847ccv4vxdmn45y50q7h7k5f nick:yard-pump sig:<60 characters>
66   t:observation f:X4PL3M state:off volt:23.8V ts:2026-08-19_18:40:00

Commanding it is section 11.7 unchanged, because an X4 station is a station:

132  t:command f:X1A67X d:X4PL3M ts:2026-08-19_18:41:00 cmd:set state:on sig:<60 characters>
141  t:result f:X4PL3M d:X1A67X ts:2026-08-19_18:41:02 r:44df54 code:200 state:on sig:<60 characters>

The allow-list of section 11.4 is kept per device, on the controller, configured by the device's owner: the neighbour trusted to run the pump is not thereby trusted to unlock the door, even when one ESP32 operates both. Everything else follows from what X4 already is. The prefix tells a receiver the station is a machine acting under someone's configuration, worth knowing before trusting its claims (section 15.5). It is self-generated, so it never originates on licensed spectrum (section 6.4.1). And a controller that dies takes its devices off the air exactly as any station goes silent, which is the honest signal: a lamp whose controller is gone IS unreachable, and nothing in the format pretends otherwise.

11.7.2 Cameras: a door you can look at

Some devices are not only read but seen. A doorbell reports that its button was pressed (state:pressed, section 11.7) and a camera reports motion (state:motion), and for both the useful next thing is a picture. A picture is not a packet: a JPEG does not fit in 250 bytes, and the format does not try to carry one. What travels on the air is a pointer, and the bytes travel over the station's LAN HTTP (the convenience interface of API-HTTP.md), fetched by whoever wants them.

The pointer is url:, the same field section 11.8 already uses to name a source. A camera carries it on its identity, so a station that hears the camera once knows where to look, and on each event observation, so the picture offered is the one from that moment:

227  t:identity f:X4DR7K k:npub1pl3m7fu9j9uenmyva7ha6x9eqwymytv2847ccv4vxdmn45y50q7h7k5f nick:frontdoor url:http://192.168.1.9/door/snapshot.jpg ts:2026-08-19_18:40:00 sig:<60 characters>
165  t:observation f:X4DR7K state:pressed url:http://192.168.1.9/door/snapshot.jpg ts:2026-08-19_18:40:00 sig:<60 characters>
164  t:observation f:X4DR7K state:motion url:http://192.168.1.9/door/snapshot.jpg ts:2026-08-19_18:40:03 sig:<60 characters>
122  t:observation f:X4DR7K state:clear ts:2026-08-19_18:40:33 sig:<60 characters>

The url: on an event names a still captured at the event, so a receiver that acts on the press has the door in front of it without a second exchange. The still behind that URL is the camera's business to keep fresh or expire.

These are broadcast, where section 11.7's doorbell addresses its owner. Both are right, and the difference is what the reading is about. Section 15 permits d: where a reading concerns one station alone, and a lock reporting that it is locked concerns the person who locked it. A camera at a shared door is the other case: the press is of use to everyone in the house, and to the station that will carry it to a phone that is out, none of whom are named on the packet. Addressing it would also put the owner's callsign on the air on every press, which section 11.9 otherwise takes care not to do, and would say who is being visited to anybody listening. A camera therefore broadcasts what happens at the door, and whoever hears it decides for themselves whether it is worth a person's attention.

Asking on demand is the request of section 8, with two words this document adds to the closed q: list: q:snapshot for a current still, q:stream for the live view. The answer is an observation naming what was asked in s: and carrying the url::

61   t:request f:X1A67X d:X4DR7K ts:2026-08-19_18:40:10 q:snapshot
180  t:observation f:X4DR7K d:X1A67X r:44df54 s:snapshot url:http://192.168.1.9/door/snapshot.jpg ts:2026-08-19_18:40:11 sig:<60 characters>
59   t:request f:X1A67X d:X4DR7K ts:2026-08-19_18:40:20 q:stream
178  t:observation f:X4DR7K d:X1A67X r:5ab0c3 s:stream url:http://192.168.1.9/door/stream.mjpeg ts:2026-08-19_18:40:21 sig:<60 characters>

q:snapshot and q:stream are reads, not actuations: they change nothing, so section 11.4 does not require the asker to sign, and a bare q:snapshot from a station in range is answered the way q:pos is. The camera still signs its own answer, because it is an X4 station and every X4 station signs what it says (section 11.7.1). Whether the URL is served to anyone who fetches it or only to a listed few is the owner's policy on the HTTP side, the same allow-list that guards an actuation (section 11.4) when a door view is private, and the general API-HTTP.md rule holds: a camera on a hostile network turns its HTTP off rather than putting a door picture within reach. The url: for a stream names whatever the camera serves, a motion-JPEG the browser can open being the plain choice, and never an RTSP address carrying the camera's own password.

11.8 Keeping a station's firmware current

A station on a roof is the case this section exists for. Reaching it with a cable costs a ladder, so it either updates over the air or it stays on the version it was carried up with, and a network of stations that can never be fixed is a network whose worst bug is permanent.

cmd:update asks a station to install firmware. It is an actuation, so section 11.4 applies in full and without exception: unsigned or unverifiable is discarded and never answered, a verified signer must be on the allow-list the owner holds or the answer is code:403, and the command expires with the 300-second window.

126  t:command f:X1RD89 d:X3P7QK ts:2026-08-20_14:26:40 cmd:update sig:<60 characters>
136  t:command f:X1RD89 d:X3P7QK ts:2026-08-20_14:26:40 cmd:update ver:1.4.2 sig:<60 characters>
165  t:command f:X1RD89 d:X3P7QK ts:2026-08-20_14:26:40 cmd:update url:http://192.168.1.9/fw/m5-1.4.2.bin sig:<60 characters>

The bare form means "install what your configured source offers". ver: pins a version, so a fleet lands on one build rather than on whatever each node happened to find. url: names a source for this one install, a mirror on the local network, or a laptop on a bench, and changes nothing about what is accepted: the image still has to carry a signature the station already trusts.

A station accepts an image, never a source. This is the rule the whole section turns on. The bytes are authenticated by a signature made by the key whose holder is entitled to publish firmware for that station, and that signature is checked before a single byte is written to storage. A hostile source, a poisoned mirror, a lying DNS answer, an operator's own mistake, can therefore waste a station's airtime and cannot change what it runs. It also means the transport needs no trust of its own: plain HTTP, a phone on the station's own access point, or a peer handing over bytes it already holds are all equally acceptable carriers.

What is signed is not the image alone. A bare digest is a hazard: the same key signs this station's packets, and a signature over 32 bytes says nothing about which 32 bytes were meant. The approval therefore covers a line that no packet can produce, a packet always begins t:, naming the product, the board, the version, the size and the image's digest together:

xprsfw1 m5stack-core 1.4.2 1340320 3f7a1c...(64 lowercase hex)

So a build for one board can never be installed on another, and an approval for one version can never be replayed as approval for the next.

The station answers twice, and the second answer is the one that matters. code:202 says the command was accepted and the work has started. Minutes and one reboot later, the station says how it went, with the same r: naming the original command:

132  t:result f:X3P7QK d:X1RD89 ts:2026-08-20_14:26:41 r:9f2c41 code:202 sig:<60 characters>
141  t:result f:X3P7QK d:X1RD89 ts:2026-08-20_14:33:12 r:9f2c41 code:200 fw:1.4.2 sig:<60 characters>
155  t:result f:X3P7QK d:X1RD89 ts:2026-08-20_14:33:12 r:9f2c41 code:500 fw:1.4.1 sig:<60 characters> m:rolled back

code:200 is aired by the NEW firmware after it has proved it works, not when the bytes finished arriving, which proves only that bytes arrived. code:500 with the old version in fw: is aired by the OLD firmware after the station put itself back, and it is the most valuable packet in this section: a failed remote update reporting its own failure, from a station nobody had to visit. code:403 refuses a signer who is not allowed, code:408 one whose command has expired, code:429 a station already installing something, and code:500 with m: a transfer that arrived corrupt.

A station that cannot go back should not go forward. The reason this is safe to do to a station on a roof is that the old firmware stays where it is until the new one has earned its place: kept, unmodified, in the slot it was running from, and restored automatically if the new one fails to come up or fails to say it is well within a bounded time. A station with room for only one firmware image may implement cmd:update, and should answer code:403 and say so, because an update that cannot be undone is a decision the operator should make with a cable in hand.

Every station says what it runs. fw: on the periodic announcement (section 13) costs about ten bytes and makes a fleet's version spread visible to anyone already listening, without asking any station anything:

117  t:service f:X3P7QK serve:archive count:1234 fw:1.4.2 sig:<60 characters>

That is how an operator finds the three nodes still on last month's build, and how a station that answers nothing at all still reports the one fact that explains why.

What this section does not do. It does not say where firmware comes from, who may publish it, or what a version string means, those are the operator's, exactly as section 11.4 says authorisation is (the allow-list itself is set on the wire by section 11.9). And it defends nothing against somebody standing at the station with a cable, which is deliberate: the owner of a device is entitled to change what it runs, and a station that its owner cannot repair is not a station they own.

11.9 Owning a station, and what the owner sets

Section 11.4 says a signature proves who sent a command and an allow-list decides whether they may give it, and until here the list was the station's private business: written into its configuration by whoever had a cable. That is the right place for the check and the wrong place for the edit. A station on a roof is set up once and then only ever reached by radio, and the person who bought it is not the person who will edit a configuration file. So the list goes on the wire, signed, as the command it always was, and with it the few things an owner decides for a station that serves other people: who may talk through it, and whose traffic leaves first.

A station with no owner asks for one. Freshly flashed, or with its configuration erased, it airs a request on its local bearers only, unsolicited and metered like any other unasked traffic (section 30):

192  t:request f:X3RLY7 q:owner scope:local ts:2026-08-08_14:26:40 k:npub1rly7x8gf2tvdw0s3jn54khce6mua7lqpzry9x8gf2tvdw0s3jn54khce6m sig:<60 characters>

scope:local because a claim is made by somebody standing next to the box, and a station that asks the whole network for an owner will be given one it did not want. k: is the station's own public key: whoever answers checks that the callsign derives from it (section 3) and that the signature verifies under it, and then holds the key section 11.10 seals a password to. A station asks soon after it starts, within a quarter of a minute, and then every two minutes, because the person who flashed it is standing there waiting.

The answer is a command, because it makes a state true:

200  t:command f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:26:50 cmd:set owner:X1QZ3N k:npub1qz3n7fu9j9uenmyva7ha6x9eqwymytv2847ccv4vxdmn45y50q7h7k5f sig:<60 characters>
145  t:result f:X3RLY7 d:X1QZ3N ts:2026-08-08_14:26:51 r:06092b code:200 owner:X1QZ3N sig:<60 characters>

The claim carries the claimer's k: for the same reason a challenge does (section 29.2): a station that has just been flashed has heard nobody, and without the key it cannot check the signature. The station checks that f: derives from k: and verifies against it. A claim without k: is verified against a key the station already holds from a t:identity, and one it cannot verify at all is code:403.

An unowned station belongs to the first signer who claims it. It accepts the first cmd:set owner: that verifies, that names the signer in owner:, and that arrived uncarried, no via:, on a local bearer, which section 11.4 already requires of a command and which here is what keeps a claim from being made from across the country. Every later claim from anyone else is code:403. There is no pairing code and no button to press, and the window in which a stranger could claim a station is the one between flashing it and answering it, which the person flashing it controls.

Ownership is a fact about the device, not about the network. Nothing is announced, nothing is recorded anywhere else, and erasing the station's configuration, reflashing it, or a factory reset, makes it unowned again and it starts asking. A station that cannot be reclaimed with a cable is not one its owner can repair, which is the position section 11.8 takes about firmware, and the same one is taken here. Selling the box is handing over the box: erase it, and the buyer claims it.

owner: is the allow-list of section 11.4, and only an owner may change it. It is a path, a comma-separated list of up to four callsigns, and the value replaces the list rather than adding to it: a list that no longer names the signer is a transfer, and a list that names four is full. A station with several owners obeys any of them, and where two disagree the later ts: wins.

owner: holds up to four callsigns and d: holds one, so a device with several owners that wants to address a report (section 15) addresses the first in the list, and broadcasts when it would rather not choose. Broadcasting is the safer default and the one a camera takes (section 11.7.2): a reading sent to one of four owners is a reading the other three did not get, and no rule here makes the first the right one.

What the owner sets, on cmd:set. The command of section 11.7 already carries its parameters in keys and answers with what IS, and a station's policy is a state like any other, so it takes three more keys and one it already has:

Key Type Meaning
owner path who may command this station: the allow-list, on the wire
use enum who may originate traffic through it: all, listed, owners, none
first path senders whose packets leave the queue ahead of everyone else's
serve words what it does for others (section 13); none for nothing
174  t:command f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:30:00 cmd:set use:listed first:X1ABCD,X1EFGH serve:relay,archive sig:<60 characters>
196  t:result f:X3RLY7 d:X1QZ3N ts:2026-08-08_14:30:01 r:6cbbb6 code:200 owner:X1QZ3N use:listed first:X1ABCD,X1EFGH serve:relay,archive sig:<60 characters>

A key that is absent is unchanged, so an owner adjusts one thing without restating the rest, and a cmd:set carrying only these keys and no state: is a complete command. The result carries all four back whatever the command carried, because a result states what IS (section 11.7), and the four together are what is. A signer who is not an owner gets code:403 for any of them, and state:, level: and target: are untouched by this section: a lamp's owner and the people allowed to switch it are not the same list, and section 11.4 still leaves that one to the device.

use: is about the bridge. A phone with no LoRa radio reaches the mesh by handing a station a packet over Bluetooth or the local network and having the station air it under the phone's own f:, and the station's owner decides who may do that. all is a public station; owners a private one; listed is the owners together with everyone in first:; none is a station that relays and archives but originates for nobody. Two exceptions hold whatever use: says, because a station that would not air a call for help from a stranger is worse than no station: t:sos and t:warning are aired for anyone. serve: is the station's announcement (section 13) made settable: it thereafter announces exactly what its owner told it to, and a station told serve:none announces nothing and does nothing for anyone, which section 22.2 says is still a good citizen.

Who goes first is fixed by this document, and the owner only fills in the names. When more than one packet waits for a bearer, a station airs them in this order, and every station that queues for others airs them in the same one:

  1. t:sos and t:warning, before anything else at all;
  2. packets whose f: is in first:;
  3. by urg:, urgent before high before normal before low, with a stranger's packet counted no higher than high (section 9.5, and the quota policy in store-and-forward.md that it cites);
  4. by ts:, oldest first.

The owner's named people rank above a stranger's stated urgency, not below it, because section 9.5 already says what a stranger's urg:urgent is worth: "stations will mark everything urgent". A name on first: was put there by somebody who answers for it. That the owner cannot move sos down, or put their own traffic above it, is the point of fixing the order here rather than offering a key for it: a station's owner is entitled to decide who it serves, and not entitled to bury a call for help under their own mail.

Anybody may ask what the policy is, so a phone learns whether a station will carry for it before spending airtime finding out:

59  t:request f:X1MB7K d:X3RLY7 ts:2026-08-08_14:31:00 q:policy
192  t:observation f:X3RLY7 d:X1MB7K s:policy owner:X1QZ3N use:listed first:X1ABCD,X1EFGH serve:relay,archive ts:2026-08-08_14:31:01 sig:<60 characters>

q:policy and s:policy are the words of section 8 doing what they always do, and the answer is an observation because that is what a state reported unasked-for or asked-for has been since section 11.7.

A policy survives a reboot, and an old one cannot be replayed. Section 11.4 gives a command 300 seconds and a station's memory of one 600, which is right for a door and wrong for a setting: a cmd:set owner: recorded today and aired next year would verify, be fresh by the ring's standard, and hand the station back to somebody who sold it. So a station keeps the ts: of the last policy command it accepted, and a policy command whose ts: is not later than that one is answered code:408, whatever the clock says now. The same rule is what settles two owners who disagree: the later ts: is the policy, on every station, in the same way.

What this section does not do. It does not put ownership on the air, so nobody can find out from the network who owns a station, and there is nothing to forge. It does not make use: a promise the station can be held to: a station that says use:all still meters strangers under section 22.2 and may answer code:429. And it does not tell a station what to do with a packet it was handed and will not air, beyond the answer it already owes, code:403, out loud, because refusing quietly is what section 22.2 forbids.

11.10 Setting up a station

A station straight out of the flasher knows nothing about where it is: not the network it should join, not what it is called, not what time it is. Somebody used to type those in over a cable. With an owner (section 11.9) they go on the wire instead, as more keys on the same cmd:set, and the person standing next to the box does it from a phone.

Key Type Meaning
ssid text the WiFi network to join; sealed
pass text its password; sealed, always
wifi enum join the network now, or off
nick nick what the station is called (section 6.3.1)
zone enum auto to look up its time zone, or an offset to pin it
ap enum its own access point, on or off
lora enum its LoRa mode (section 14.8): xprs, meshtastic or meshcore; taken at once
freq text the LoRa channel, the shape section 14 writes one in (433.900MHz, the suffix optional) or in hertz, or preset for the region's own; taken at once
region enum a channel preset of the running mode (eu, eu-433, us, au), which brings its frequency, its hour and its ceiling
key enum new: make a new key, and with it a new callsign
nsec bech32 take this key instead; sealed, always

Only an owner may set any of them. A station that is not owned answers code:403, and so does one owned by somebody else.

A password is never in the clear. pass: and nsec: are accepted only inside x: (section 11.4), sealed to the key the station aired on its q:owner; one in the clear is code:400 and is not stored, because a Bluetooth advertisement is a broadcast and everybody within a hundred metres has just read it. ssid: travels with its password:

t:command f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:31:00 x:<107 characters> sig:<60 characters>

225 bytes. Sealed inside are three lines, cmd:set, ssid:Casa do Mar and pass:sardinha na brasa 2026, 52 bytes, and the lines are what let both values keep their spaces. A network name and a password of up to 79 bytes together fit one packet; a longer pair goes as two commands, ssid: then pass:, and the station joins when it has the second.

Joining takes two answers, because joining takes seconds and can fail:

161  t:result f:X3RLY7 d:X1QZ3N ts:2026-08-08_14:31:01 r:b4a13e code:202 wifi:joining ap:on zone:auto sig:<60 characters>
172  t:result f:X3RLY7 d:X1QZ3N ts:2026-08-08_14:31:09 r:b4a13e code:200 wifi:up ip:192.168.1.40 ap:on zone:auto sig:<60 characters>

The first says the command was taken, the second what came of it, both naming the command by r:. A join that fails ends code:500 wifi:failed with the reason in m:, wrong password, no network by that name or could not join, which is the one sentence the person holding the phone needs:

t:result f:X3RLY7 d:X1QZ3N ts:2026-08-08_14:31:31 r:b4a13e code:500 wifi:failed ap:on zone:auto sig:<60 characters> m:wrong password

177 bytes.

A station that was on a network keeps it until the new one has joined. After the failure it goes back to the network it had, and only then answers, from there: the 500 then reads wifi:up ip:<the old address> beside the reason the new one failed. A station reached over the very network being changed would otherwise air its 500 on a link it no longer had, and a typo in a password would have cost the owner the station.

A result states what IS (section 11.7), and for a station being set up that is wifi: (up, joining, failed or off), ip: while it is up, ap:, lora: if it has a LoRa radio, nick: if it has one, and zone:. It never repeats the network name or the password: the owner already knows them, and everybody else should not.

158  t:command f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:32:00 cmd:set nick:roof-north zone:+01:00 ap:off sig:<60 characters>
191  t:result f:X3RLY7 d:X1QZ3N ts:2026-08-08_14:32:01 r:92d70e code:200 wifi:up ip:192.168.1.40 ap:off nick:roof-north zone:+01:00 sig:<60 characters>

ap:off is answered before the access point goes, because the answer may be leaving by it.

The channel is a setting, not a property of the hardware. The same radio is sold matched for 433, 868 and 915 MHz, and a network's presets differ by region and change over time, so an owner sets the frequency the way they set anything else: region: takes a preset of the running mode, freq: takes an exact channel for a band the station has no preset for, and freq:preset gives the region's own back. Both are taken at once, and the answer carries freq: and region: so the owner reads back where the station landed rather than trusting that it heard them. A frequency the radio cannot tune is code:400; a region the running mode does not have is code:501.

What a station cannot answer for is whether a channel is legal where it stands. It meters against the region it was given, it says in its log when a frequency sits outside that region's band, and the rest belongs to the operator, exactly as the power ceiling does (section 14.4).

The LoRa mode is taken at once (section 14.8): the radio retunes, the airtime budget follows it to the new channel, and the station stays up. It is true by the time the answer leaves, so the answer is a code:200 naming the mode now running. A mode the station's firmware does not have is code:501 and a switch that failed is code:500, both with nothing changed. A firmware may leave a mode out: a board too small for a second mesh has the vocabulary and not the code, and 501 is how it says so:

133  t:command f:X1QZ3N d:X3RLY7 ts:2026-09-19_20:00:00 cmd:set lora:xprs sig:<60 characters>
201  t:result f:X3RLY7 d:X1QZ3N ts:2026-09-19_20:00:01 r:5c19e2 code:200 wifi:up ip:192.168.1.40 ap:off lora:xprs nick:roof-north zone:+01:00 sig:<60 characters>
242  t:result f:X3RLY7 d:X1QZ3N ts:2026-09-19_20:00:01 r:5c19e2 code:501 wifi:up ip:192.168.1.40 ap:off lora:meshtastic nick:roof-north zone:+01:00 sig:<60 characters> m:meshcore is not in this firmware

From that moment the station is on another channel: a station in another mode no longer hears it on LoRa, and it is reached, as it was set, over every other bearer. Whatever was in flight on the old channel is lost, which is why the mode is an operator's decision and never an automatic one.

A new key is a new station, and the phone follows the key. The callsign derives from the key (section 3), so key:new or an nsec: changes the callsign too, and a station takes a new key only at a restart. It answers first, with the new public key and signed with the old one, restarts, announces itself, and answers again under its new name:

131  t:command f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:33:00 cmd:set key:new sig:<60 characters>
198  t:result f:X3RLY7 d:X1QZ3N ts:2026-08-08_14:33:01 r:6e945a code:202 k:npub1m4q8ce6mua7lqpzry9x8gf2tvdw0s3jn54khce6mua7lqpzry9x8gf2tvd sig:<60 characters>
173  t:identity f:X3M4Q8 ts:2026-08-08_14:33:12 k:npub1m4q8ce6mua7lqpzry9x8gf2tvdw0s3jn54khce6mua7lqpzry9x8gf2tvd sig:<60 characters>
222  t:result f:X3M4Q8 d:X1QZ3N ts:2026-08-08_14:33:13 r:6e945a code:200 k:npub1m4q8ce6mua7lqpzry9x8gf2tvdw0s3jn54khce6mua7lqpzry9x8gf2tvd wifi:up ip:192.168.1.40 sig:<60 characters>

The 202 is the old station vouching for the new key, and the 200 under the same r: is the new one proving it holds it. The 200 carries the key itself, and the callsign derives from it, so a phone that heard neither the 202 nor the announcement can still believe it; it says where the WiFi stands and no more, because the rest does not fit beside a key. Until the command's window closes, a repeat of it still addressed to the old callsign is answered again from the new one. Its owners stay its owners. An imported key travels sealed and alone:

t:command f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:34:00 x:<128 characters> sig:<60 characters>

246 bytes: cmd:set and nsec: with its 63 characters, 76 bytes sealed. That is how a station that died is replaced by one that carries on under its callsign.

A station being set up has no clock, which rules out the 300 seconds of section 11.4 for these commands: nothing on the box can tell how old one is until it has joined a network. They follow the rule policy commands already follow (section 11.9): a command whose ts: is not later than the last one the station accepted is code:408, and the station keeps that ts: across a restart. A command it has just accepted, heard again, is answered again with what now IS, at most once every fifteen seconds, because a Bluetooth advertisement repeats until its sender stops it and a retransmission is not a replay. Setup keys and policy keys are not mixed in one command, and one that mixes them is code:400.

Where a clockless station gets the time. From the network, once it reaches one, and until then from its owner: the ts: of a command that verified against the owner's key is the best guess the box has, so it takes it, and the rule above already makes each later than the last. The network's time replaces the owner's the moment it arrives, and nothing replaces the network's. Nobody else's ts: moves a clock. A stranger's packets date themselves, never the station, because a station whose clock can be set from the air can be made to date its own words wrongly, to refuse its owner's commands as stale (section 11.4), and to carry old traffic as news. A station that must run with neither network nor owner in range may be set to take, while it has no clock at all, the time several verified neighbours agree on; that is a choice its owner makes, not a default, and it only ever fills a void. A station that obtains the time sends one packet carrying both forms (section 15.7).

Every answer goes back the way the command came. A phone that sent a command over Bluetooth is listening on Bluetooth, and one on the station's own access point is on that network. A station without Bluetooth, and a phone without a working one, do all of this over the access point: the phone joins it and the same packets cross it on UDP 4242, as section 13.4 carries any other.

What this section does not do. It gives the station's owner no way to read its password back, and gives nobody but an owner any way to make it decrypt anything. It does not make a claim safer than section 11.9 does: the first person to answer an unowned station owns it, and the person who flashed it closes that window by answering first.


Part III. Archiving and services

Who keeps what and who does what for the network: the archiver, the service words a station advertises, channels, and the observations stations air.

12. Archiver

Publishing, and the archivers you choose.

An archiver keeps XPRS packets and answers for them. It holds a spool of what it heard and re-airs it on cmd:history, keeps the publications of the stations that chose it, holds mail for stations that are absent, and publishes a directory of who-keeps-what. The goal: a message, a post or a file survives its author going offline, and an absent station stays reachable.

Every device is an archiver; scale is a setting, not a kind. There is no server class. A pocket phone archives its operator's own words and those of the callsigns they follow, and announces nothing. A powered station archives everything it hears, carries mail, bridges bearers, and announces serve:archive. An always-on internet machine does the same at larger budgets. Archivers differ in what they answer, never in what they are, and a reader learns which is which by asking, so the format needs no word to rank them.

The operator sets the rules, like a NOSTR relay operator. The owner decides what it keeps and for whom: their own traffic, the callsigns they follow, stations heard nearby, or anyone, under a quota. Silence is not consent: a station holds nothing for strangers until its operator says so (section 22.2), and may decline anything offered. Holding is what that sentence governs: keeping the three room conversations a station shows its own operator (section 9.11.1), under a quota, makes it nobody's archiver and obliges it to serve nothing.

Three settings say it, and one implementation (the XPRS app) names them the way a person would: what is mine is kept unconditionally; the people I follow are the middle tier, kept for as long as they are followed, with no quota and no expiry; and a public archiver additionally keeps strangers' packets within a byte cap and a maximum age, and is the only one of the three that puts serve:archive on the air. A station with the third setting off announces no archiver role at all and answers a stranger's cmd:history only from its own publications — which is the zero-archiver configuration two paragraphs down, arrived at from the other side. The follow list itself never leaves the device (section 16.2): a peer sees what the station keeps, never for whom.

The depositor chooses too. Unlike APRS-IS there is no centre: a station keeps its own publications and hands a COPY to the archivers its operator chose (section 34.3). No station must name one, and no archiver sees traffic from a station that did not pick it. A station naming none MAY let one be auto-selected, a volunteer advertising serve:archive that accepts it; naming one explicitly overrides that. Zero archivers is a valid, private configuration: such a station serves its own publications to whoever asks over the radio, and is simply not findable by someone who was not listening.

(Synchronisation BETWEEN archivers is out of scope here and specified separately.)

12.0 One role, and it does not change with the bearer

serve:archive is the only word, and covers everything: keeping packets and answering for them (sections 12.1 to 12.6), holding mail (section 12.7) and saying how much (sections 15.6.5 and 9.12.3), delivering it when the recipient turns up (sections 12.8.1 and 12.8.2), and publishing the directory (section 12.9). There is no separate word for the pointer half or the storage half; a pointer-only station and a keep-everything station announce the same word and differ only in what they answer. An operator's interface MAY present the offers separately, and should, since holding somebody's files and remembering where their files are cost very different things and either may be granted alone. On the air it is one word.

The role is identical on every bearer. A cmd:history over Bluetooth, LAN, ESP-NOW, LoRa or the internet gets the same records for the same window, the same code:202 / code:200 / code:206 / code:404 / code:429, the same paging, budgets (section 22.2) and signatures. A bearer decides how bytes travel, never what a station will do; an archiver that behaves differently on one is broken, not slower. This is written down because the failure is invisible: a station that answers on one bearer only still beacons, still archives, still asks its own questions correctly, logs no error, and every asker on another bearer just hears silence.

An offer covers every bearer, not the one it was heard on. serve:archive offers to archive on ALL of a station's bearers. A reader that hears the offer over the LAN but can reach the station only over LoRa still asks over LoRa, and is owed the same answer.

The one place a bearer legitimately decides anything is choosing among several paths to the SAME station. An archiver with more than one path to its asker picks the highest usable bandwidth among those it has recent evidence are working; reliability outranks raw speed, because a fast path that has carried nothing lately is a guess and a slower one that answered a minute ago is knowledge. The bearer an ask ARRIVED on is therefore a sound default: it just carried a packet from the station now waiting, the freshest evidence of a working path. But it is a default, not a constraint. If the archiver knows a better path it uses it, and if the arrival bearer cannot carry the answer it does not give up.

Where a station cannot tell which path reaches the asker (the broadcast case), it answers on every bearer it can transmit on. Duplicates deduplicate on the section 5 identifier, so the reader sees one copy however many arrived. On an expensive bearer an archiver MAY hold back a page it already sent somewhere faster; it MUST NOT hold back a code:404 or code:429, because a control packet that does not arrive is indistinguishable from a station that is not there.

A claim must not silence proof. A link: (section 15.6.1) is a station's own word that it stands on a radio; it is a claim, not a measurement that you share that radio. An internet path carries no link:, because it is not a radio a station stands on, and its only evidence is a live route the transport holds. Until via: distinguishes a beacon heard on the air from one relayed home (section 37, not yet implemented), a station on another network advertises link:ble into your ear as though in the room, so a path merely claimed MUST NOT suppress one you have confirmed: when both stand and nothing confirms the claim, answer on both.

None of this is scope:local (section 9.11.1), which is the author's property saying how far a packet may travel, and it constrains the answer like anything else. What this section forbids is a station narrowing its OWN service to one bearer of its own accord.

12.1 What a publication is

An archiver holds two things, and the difference is d:.

What it is What the archiver does with it
A publication, no d:, a type below offered to whoever is interested answers queries about it, to anybody
Mail, anything carrying d: addressed to one station holds it for that station and tells them; never offers it to a third party (section 12.7)

Publication types: blog, passage, event, offer, need, place, poll, track, warning, info, status, channel, service, file, sos, observation, identity.

The last two make a gateway useful beyond its own hill. A gateway's own t:observation is a reachability record: f: says which gateway, hears: the radio-only stations at its ear now, ts: how fresh the claim is. And a gateway passes on the t:identity and t:mailbox packets it hears verbatim, which section 34.2 makes safe: the author's signature travels with the packet, so a copy proves itself against the author's key, not the gateway's honesty.

Neither published nor held: ping and pong, which measure whether a path is alive right now.

Addressing decides, not the type. The packet already says who it is for, so a t:message is mail, a t:command to a sleeping station is mail, and a t:warning with no d: is a publication.

12.2 The archiver is sent the packet, not a description of it

An archiver receives the publication exactly as it was composed and signed:

t:warning f:X3RLY7 pos:39.40,-8.20 rad:5km dest:38.72,-9.14 near:40km urg:urgent kind:fire sev:danger until:2026-08-10_00:00:00 ts:2026-08-08_14:26:40

150 bytes, the section 24 example, unchanged. There is no envelope, no summary record and no transformation step, hence no second schema to keep in step with the first and get subtly wrong.

Why this is affordable. The file layer stores pointers and never content because a file is megabytes and a pointer 176 bytes. A publication is at most 250 bytes: pointer and content are the same order of size, so the rule is not "always send pointers" but "send whichever is smaller", and here that is the packet. The inline file lane (section 7.7.4) applies the same rule: under ~900 bytes the content IS the smaller thing, and it rides the packets.

Everything a query needs is already in it: t: the type, f: the author, ts: when, pos: where, dest: and near: the region, until: expiry, scope: reach. And the signature travels with it, so an archiver passing on a third party's publication can neither forge, retarget nor resurrect it, and the receiver verifies against the author's key (section 7.1). That is what makes gossip between archivers safe.

12.3 You choose your archivers

A station pushes to the archivers its operator picked, and to no others.

The station remembers what each archiver has already had as a position in its own log rather than a time (a position cannot skew, and a clockless device can still persist one). A reconnect resumes; it does not re-send.

12.4 When to push

When there is a link and it is cheap. Publishing is not urgent traffic: a blog post that reaches the index four hours late has lost nothing, and a station that spends its LoRa duty cycle pushing publications has spent it on the wrong thing (section 22.1). So the push belongs to a bearer that is cheap and plentiful, internet or a wired link, and the radio carries publications the way it always did: as packets, to whoever is listening, once.

12.5 Asking a station directly

A station that heard something and wants the rest asks the author, with a verb that already exists:

t:command f:X1BOA3 d:X3RLY7 ts:2026-08-08_14:26:40 cmd:history since:2026-08-04_00:00:00 sig:<60 characters>

153 bytes (section 11.2). Already metered by the airtime rules, already refusable with code:429, and already the answer to "I was not here, what did I miss". Publishing adds no verb of its own.

12.6 What replaces the filter

An APRS-IS client subscribes with a filter and receives a stream. Here a reader asks an archiver a question and gets the packets that answer it: by author, type, region and radius, or time window. Each reads a field the packet already carries, so the query surface needs no vocabulary of its own and cannot drift from the format it queries.

One reading rule makes the reachability question askable without any new word: only: matches a callsign wherever the packet carries it, as author, as addressee, or inside a list field (hears:, hold:, via:, grant:). It is a callsign and never a type; section 11.2's kind: names a type, and the two combine rather than compete. So "everything about X1BOA3" includes the gateway observations that list it as heard:

165  t:command f:X1QZ3N d:X3IDX1 ts:2026-08-17_14:00:00 cmd:history only:X1BOA3 since:2026-08-17_13:00:00 sig:<60 characters>

The reply is the section 11.2.1 replay, code:202, then the original packets, code:200, and among them:

169  t:observation f:X3RLY7 link:lora peers:6 hears:X1BOA3,CT1ABC-9,X5A3F2 uptime:9day ts:2026-08-17_13:59:20 sig:<60 characters>

X1BOA3 was at X3RLY7's ear forty seconds before the ask. The reader now knows which internet-connected station is one radio hop from the recipient, and how stale that is (ts: is the freshness, and a reader that gets three gateways back prefers the newest). A miss is not a dead end: an archiver that does not archive the callsign answers code:404 with m:try naming peers whose directories list it (section 12.9).

What matters is not the syntax but that the reader chose the archiver, the publisher chose the archiver, and neither choice had to be the same for the network to work.

12.7 An archiver is also a mailbox

The sender is usually gone: a message written on a phone that then pockets its radio. If delivery depended on that phone still being reachable when the recipient next wakes, most messages between people not simultaneously awake would never arrive. Store-and-forward exists for this (section 6.3), and an archiver is its best carrier: always on, addressable, chosen deliberately. So mail is handed to an archiver too, which tells the recipient something waits, and an archiver is a natural entry in a station's hold: list (section 9.12), which already exists and needs nothing added.

Privacy is the content's problem, and the format already solved it. Seal the body with x: (section 7.2) and the archiver stores something it cannot read:

t:message f:X1QZ3N d:X1RD89 ts:2026-08-13_10:14:00 x:pQ4m9xT2vB8kR until:2026-08-20_00:00:00 sig:<60 characters>

157 bytes. t:, f:, d: and ts: stay in cleartext, because a station that cannot see who a packet is for cannot deliver it. So the archiver learns that X1QZ3N wrote to X1RD89 at that minute and how often the two do that, exactly as every APRS-IS server did for every message. Encryption protects the contents; choosing your archiver is what protects the pattern.

It is a hold, not an archive. until: bounds it, and the archiver releases its copy the moment it hears a receipt whose signature it has verified (section 8; section 9.7.1 insists on the signature so a stranger cannot delete other people's undelivered mail). An archiver may refuse to carry mail at all, or carry it only for stations it knows (section 22.2).

12.8 The gateway is the last mile

Sections 12.1 and 12.6 built the outbound half: the gateway told the archiver who it hears, and a sender found the gateway. This is the return leg, how mail deposited at an archiver reaches a station that never touches the internet. Mail is never offered to a third party (section 12.7), and a gateway asking for somebody else's mail IS a third party; section 9.12 solves it only for stations that declared a mailbox, which a solar tracker on a ridge cannot do.

The resolution splits on what the mail is:

Delivery is then ordinary. The sender seals and deposits:

162  t:message f:X1QZ3N d:X1BOA3 ts:2026-08-17_14:02:00 x:pQ4m9xT2vB8kRZ7cW0yLuJ3gRhN8sEiDoQ6vXaB1MnYw sig:<60 characters>

The gateway X3RLY7, whose observation listed X1BOA3, collects it and airs it on the radio under the custody rules of section 9, and the receipt comes back the same path:

130  t:receipt f:X1BOA3 d:X1QZ3N r:b47210 ts:2026-08-17_14:19:12 s:ack sig:<60 characters>

The recipient signs the receipt, so the archiver verifies it and releases its held copy (section 12.7), the gateway archives its own, and the sender three networks away knows the tracker on the ridge has the message. The gateway was trusted with nothing but ciphertext and effort.

12.8.1 The return leg, automated

Section 12.8 built the last mile by hand: someone had to ask. Two behaviours remove the hand, one MUST and one MAY, and together they make a deposited message ARRIVE rather than wait to be collected.

A station holding mail for X delivers it the moment it hears X directly (section 15.6.3's sense, no via:), on the bearer X was heard on, the freshest evidence of a working path (section 12.0). This is not a poll: the trigger is the packet from X itself, not a ten-second check that spends battery on a question the air already answered. Retries of an unacknowledged copy back off (the same packet re-aired), which is NOT the same as handing over the NEXT ones (section 12.8.2, which should be prompt). A verified t:receipt (section 9.7.1) ends the retries and releases the held copy, and every OTHER holder that hears the receipt releases too, so a chain of custodians drains instead of delivering twice. Airtime spent on delivery is metered under section 30.

A station holding mail for X MAY hand it toward where X actually is, in order:

  1. X's own declaration (section 9.12): the recipient's word beats every observation. This is the first time hold:'s preference order is consumed rather than merely stored, and it is consulted first.
  2. Every gateway whose live sighting of X is fresh (section 12.9.4's L3, inside its TTL): a station that heard X five minutes ago on LoRa is one hop from delivering, whatever continent this holder is on, and when three stations say so, all three get a copy. Each copy is its own custody trail and costs the holder one directed send; the one receipt X signs releases every copy (section 9.7.1), so the extra copies buy delivery by whichever gateway actually meets X and cost nothing afterwards. The place X was LAST seen (L2) is used only when nothing is live, and only the freshest of it.

The hand-off is CUSTODY (section 6.3), not content sync: the author's packet travels byte for byte with its signature, via: gains the holder's callsign, the section 6.1 budget and 6.2 loop check apply, and section 12.8's disclosure rule stands. Once per holder and gateway: a packet is forwarded one time to each gateway, one that comes back (per via:) is not forwarded again, and a gateway that reports X LATER still gets its copy, because the report is the event the holder was waiting for. A holder that can reach X itself forwards nothing: it is the gateway, and the first paragraph delivers. Without that rule a gateway handed a copy over the internet asks an always-on archiver where X is while X is beaconing at it. This migrates the mail toward the recipient's radio horizon while both parties sleep, the whole difference between a mailbox and a pile.

And the gateway must have said so. A hearing that stays on the radio informs nobody on the internet. A gateway therefore deposits its own signed t:observation with the archivers it chose (section 12.1, as an addressed copy each, section 12.12.2), at once when a station appears at its ear that its last report did not name and otherwise on a period, together with its t:identity so the claim verifies (section 12.9.4: an unverifiable claim feeds nothing). A gateway that hears nobody deposits nothing.

12.8.2 Draining a backlog, and what a holder must remember

A returning station has more than one message waiting, and both obvious readings of "delivers it" are wrong: airing everything at once spends the channel the station is trying to use, and airing one per sighting takes as many sightings as messages.

12.8.3 The whole loop: what the three stations owe each other

Sections 12.3, 12.7 and 12.8.1 each describe one station's duty. Written apart they read as complete, and an implementation can satisfy every one of them and still lose the message — which is what happens, so the loop is written here as one thing, in the order it runs.

A writes to B. B is asleep. A is a telephone, and goes into a pocket.

  1. A leaves a copy with its archiver. A directed packet that A originated and that nothing has acknowledged is copied to A's archivers, unchanged and with its signature intact. This is the step whose absence loses the message: a retry ladder on A cannot help, because A is the station that leaves. It is not a relay — via: is untouched and the section 13.1 hop budget is not spent — it is the author handing a copy to the station it chose to keep its mail. The wait before depositing is an implementation's own: long enough that a delivery in progress is not duplicated, short enough to happen before the radio is put away.

  2. Any packet, not only a message. The archiver holds what it is given, and what a station gives it is everything addressed to one station that is worth having later: the message, the t:file that describes a picture, the t:moderate that adds somebody to a group, the t:receipt that closes a loop. Two kinds are excluded, and both for the same reason — they are true only while they are fresh. Presence (t:observation, t:identity, t:service, t:track, t:ping/t:pong, t:status) says where a station is now; delivered an hour later it is a statement about a moment that has passed. A live negotiation — a cmd:file carrying the have: map of what the asker lacks right now, or the t:result answering one — asks for bytes on behalf of somebody who has stopped waiting. scope:local is never held at all (9.11.1), and a group has no mailbox (12.8.1).

  3. The archiver delivers when it can reach B, not only when it hears B. Section 12.8.1's trigger is the packet from B itself, which is the right evidence on a radio. An always-on archiver on the internet mostly never hears its recipients speak: it simply gains a path to them. So a holder also asks, on its own schedule, "who am I holding for, and can I reach any of them now" — and a holder that only ever waits to be spoken to keeps internet mail for ever while the recipient sits online three hops away. Reachability means THIS station: "somebody is on the network" makes every callsign on earth look present, and a holder reading it that way re-airs mail to callsigns that have never existed.

  4. Delivering is relaying, so the holder signs it as one. via: gains the holder's callsign (12.8.1). That costs one field and buys the two things custody needs: the loop check, and — the reason the loop used to hang open — B learns that somebody else is holding a copy.

  5. B's receipt goes to more than A. A verified t:receipt releases every holder that hears it (13.3), and the one station guaranteed not to be listening is A, who is asleep — which is why an archiver had the message at all. So B sends the same signed receipt, unchanged, to: - the stations named in via:: they carried it, so they hold it; - the mailboxes A declared with t:mailbox hold: (13.12): A deposited there, and those copies will be re-delivered until told otherwise.

The receipt is never re-addressed on the way. d: is inside what the signature covers, and 9.7.1 requires that signature precisely so a stranger cannot delete other people's mail; a holder does not need to be the addressee, because releasing on an OVERHEARD acknowledgement is exactly what 13.3 describes.

  1. The receipt is itself mail. It is addressed to A, who is still away, so the archiver holds it like anything else and delivers it when A returns. Without this the sender never learns the message arrived, and keeps a pending message on screen for a delivery that happened days ago.

  2. A station says where its mailbox is. None of step 5 works if nobody knows where A's mail is kept, so a station that has archivers declares them with t:mailbox hold: (13.12) and re-declares when the list changes. A declaration is signed; an unverifiable one is ignored, because a forged one collects somebody else's mail from every polite sender.

Choosing the archiver is step zero, and it must not need an operator. Section 12.3 leaves auto-selection optional — "a volunteer advertising serve:archive that accepts it" — and a station that names none and adopts none has nowhere to leave anything, which is the whole failure again with an extra step. So a station with no archiver of its own SHOULD adopt one it can hear, preferring a volunteer reachable over the internet to one reachable only over a radio (leaning on a station you have to stand next to is not leaning), and SHOULD KEEP the one it adopted: an archiver re-picked every hour spreads one conversation over five stations and none of them holds the whole of it. An archiver the operator names explicitly overrides the adopted one, and zero archivers stays a valid, private choice (12.3).

What each station may assume. A has no guarantee its message was delivered until a receipt says so, and no guarantee an archiver kept it. B has no way to tell a message that came direct from one that waited a week, and needs none. The archiver never learns what it held: the body is sealed (12.7), and what it can see — who wrote to whom, and when — is what any station forwarding a packet can see. None of the three has to be awake at the same time as another, which is the only property that matters.

12.9 Archivers among themselves

An archiver never accepts content from another archiver. This keeps a federation of archives from becoming one pool of spam. A peer's archive is that peer's admission decisions, and bulk-importing it imports every decision the other operator got wrong, at zero cost to whoever made them. Content enters an archiver exactly one way (section 34.3): from the callsigns its operator chose or agreed to receive. (The gateway pass-through of section 34.1 is the same rule: the gateway is a depositor this archiver accepted.)

What archivers DO exchange is a directory: which callsigns each is archiving or receiving from, and when it last heard each. A text listing in the section 7.7.2 family:

XDIR1
CT1ABC-9 2026-08-17_13:40:11
X1BOA3 2026-08-17_14:02:36
X1QZ3N 2026-08-17_14:05:02

One line per callsign, call then ts, sorted by callsign. The directory is an ordinary content-addressed file: named in its archiver's signed announcement, fetched with cmd:file, verified against its reference like anything else.

184  t:service f:X3ARC1 serve:archive count:212 file:qA7dTf2mWx9bK4pZcV0yLuJ3gRhN8sE5iDoQ6vXaB1M.xdir ts:2026-08-17_15:00:00 sig:<60 characters>

count: stays what section 13.0.1 made it, records held, never callsigns; how many callsigns the directory lists is learned by fetching it. The economics are section 34.2's: a line costs about 28 bytes, ten thousand callsigns about 280 kB, and an unchanged directory has an unchanged hash, so polling a quiet peer moves nothing. A consumer stores pointers (callsign, which archiver, how fresh), never the content behind them. A false directory line is priced like a false hears: (section 15.6.3): one wasted redirect per reader, because the content still answers or fails on its own signatures.

Discovery needs nothing new. A station finds an archiver four ways: serve:archive heard on the air; another archiver's copy of that same signed announcement (section 34.2 makes verbatim relay safe); cmd:history kind:service against any known archiver, which replays every t:service it holds; and the redirect that answers a miss:

152  t:result f:X3ARC1 d:X1QZ3N ts:2026-08-17_15:04:10 r:5fd021 code:404 sig:<60 characters> m:try X3ARC2,X3ARC7

An archiver asked about a callsign it does not archive says so plainly and names, in m:try, the peers whose directories list it (the section 11.2.1 alternates for 429, extended to the miss). The reader asks the named peer directly; the first never proxies, because proxying is how content would cross the line this section drew. The result is a federation of small archives, each vouching only for what its operator chose to keep, joined by directories that say who keeps what.

12.9.1 What crosses between archivers

Everything archivers exchange is a signed original, verifiable against its author's key rather than the peer that handed it over (section 34.2). Four things cross:

What crosses The question it answers
a t:service announcement, verbatim who offers what; how an archiver is found at all
the directory file (section 12.9) which callsigns deposit where, and when each was last heard
the archiver's own t:observation, with hears: which stations are at its ear now, and how fresh that claim is
t:mailbox hold: declarations, verbatim which stations chose which archiver to hold their mail

The last three ARE the gossip of section 12.9.4, reachability carried by packets that already existed. Each is signed by its author, so a relaying peer can neither forge nor retarget it, and a reader weighs its ts: for itself.

What does not cross is the store. Archivers do not synchronise message archives; the directory exists precisely so they need not. The one temporal fact that flows is the last-heard timestamp riding each directory line. And mail never crosses at all: a held message leaves an archiver toward its recipient, a declared holder, or a hears: gateway (sections 12.7, 12.8), never toward a peer archiver.

12.9.2 The archiver is the seeder

Publications carry file: references (section 7.7); the bytes behind a reference live wherever somebody kept them. For those bytes the archiver plays a torrent seeder's part: it holds a full verified copy, says so when asked, and serves it without involving the author.

The mechanics exist and this adds none: q:have (section 8.1) asks whether the bytes are held; cmd:file fetches them, off: resumes a dead transfer; cmd:put deposits them under a stated size: and until:; an ih: reference (section 7.7.5) lets the same copy seed a BitTorrent swarm. A station doing this announces serve:files beside serve:archive, since a pure file host with no spool is a real station, but on an archiver the two halves reinforce: the spool holds the announcement that names the file, the file store holds the bytes it points to. Admission still follows section 34.3, not the swarm. The directory file of section 12.9 is itself served this way, the arrangement eating its own cooking.

Finding the bytes across the network. q:have is answered by whoever hears it, which is a neighbourhood. The wider question, where a file can be found when no holder is in earshot, is answered by the same federation that routes callsigns (section 12.9). A holder advertises each file it keeps as a signed provider record, the hash then the holder, verifiable against the holder's key like every other claim; large archivers exchange those records among themselves the way they exchange the callsign directory, so a hash learned anywhere resolves to a holder somewhere. A q:have an archiver cannot answer from its own store it answers from that index, naming holders in m:try the way a cmd:history miss names them. Small archivers do not carry the whole index: they report the hashes they hold UPWARD to the large archivers they chose, on the same push that carries their publications (section 12.3), and pull a resolution back when a duty needs one. No archiver holds every file and none holds the whole index; each vouches only for the bytes it kept and the records it signed, which is the seeder-and-tracker split a BitTorrent swarm makes, done with signed claims instead of a central tracker.

12.9.3 Neighbours: redundancy for an area

Most of what a community publishes is about a PLACE (info, warning, event, status, blog, and a town channel's undirected message traffic). A neighbourhood whose only archiver went dark loses its noticeboard, so two archivers placed near each other MAY keep each other's community publications alive, within bounds:

t:command f:X3ARC1 d:X3ARC2 ts:2026-08-20_08:30:00 cmd:history kind:info,warning,event,status,blog,message since:2026-08-20_06:12:44 sig:<60 characters>

This is section 12.10's meeting with a kind: filter, on section 12.10.1's per-peer watermark, under section 22.2's budgets, a standing catch-up rather than new protocol. The peer re-airs ORIGINALS, each verifying on its author's signature, entering the asker's store through its own admission; section 12.9's line is not crossed, because nothing is imported on the peer's authority. Replayed packets deduplicate on the section 5 identifier and expire on their own until:, so the two stores converge on the union of both neighbourhoods' publications for those kinds and nothing else. Mail takes no part: a d: packet is section 12.7's business however it travels.

12.9.4 Gossip: who was heard where

The exchanges above have a collective name: gossip. Archivers never sync content, that rule opened this section and stands. What they gossip is REACHABILITY: which callsign was heard, by which station, on which bearer, when. Each archiver keeps its own bounded table, built from what it overheard and what peers passed on, and the network's answer to "where can X be reached" is the sum of many small tables rather than one central one. That is the load-bearing difference from APRS-IS, whose central servers hold the only copy of exactly this.

Gossip rides existing packets, a signed t:observation with hears: (15.6.3), a signed t:mailbox (9.12), a t:service announcement (24), relayed verbatim under section 34.1. There is no gossip packet type, because a new envelope for old facts would be a second schema to drift.

The three layers, consulted in this order:

layer holds fed by expires
L1, declared the callsign's own hold: list, in its order its own signed t:mailbox its own until:
L2, visit history the last K distinct archivers that heard the callsign DIRECTLY on a short-range bearer; per entry: archiver, first heard, last heard radio truth only (below) never, the ring evicts the oldest distinct archiver. K is 100 for the reference station classes
L3, live sightings the freshest (gateway, bearer, time) claims, at most G per callsign (reference G = 8) any verified sighting, internet included a TTL (reference 24 hours)

L1 is the recipient's word and beats everything; L3 answers "deliver NOW"; L2 answers "where does this callsign tend to surface", the marina a boat checks in at, the repeater a commuter passes, the knowledge that routes a message deposited months after the last sighting. L2 is the one layer allowed to live forever, because it is small (K entries per callsign), slow-moving and irreplaceable.

Validity: gossip is a priced claims market. Every entry is credited to the station that SIGNED the packet it came from; unsigned observations feed nothing beyond the local air view, and forged ones are dropped by the existing verification. Three walls:

What spam buys after all three walls is what a false hears: always bought (section 15.6.3): one wasted redirect per reader, never a delivery.

The arithmetic. An entry packs into about 20 bytes; a full K=100 visit ring is ~2 KB per callsign; a pocket or desktop node's reference cap is 5 MB, room for the visit history of ~2,500 callsigns, which is a town. Stations below that class do not try; stations above it raise the cap.

Need-to-know, not replication. A station keeps gossip in proportion to its duties. An ESP32 archiver keeps the L1 declarations that name it, its own direct-heard rings, and L2/L3 rows only for callsigns it is currently holding mail for, a few kilobytes; everything else it resolves when a duty arrives, by asking a bigger archiver: cmd:history only:X1BOA3 kind:observation (section 12.6) replays the signed sightings, which the asker verifies and caches into its L3. Bulk gossip is the history replay; it needs no new verb. (The XDIR census file is the bulk form for a station that can serve files; one that cannot omits the file: reference from its announcement and the ask-form carries the load.)

What an always-on archiver must be. Scale is a setting (section 12.0), not a class, and serve:archive is the only word. But the archivers the network actually leans on share qualities a pocket device cannot promise, and a station should not invite asks it cannot serve (section 22.2):

the quality what it means in practice
addressable reachable by a DIRECTED packet, not only broadcast; an archiver that cannot be asked cannot be leaned on (12.12.1)
deep a spool measured in RECORDS, not in leftover flash; a store holding only a busy day is a pocket archiver, honest as one
budgeted for it section 22.2's reference numbers raised as far as the machine allows, because six replays an hour is a pocket budget
concurrent more than one ask in flight, or the second asker of any minute is refused
complete gossip kept for every callsign it learns of, without the need-to-know cap a small station applies
awake always on, so it heard what everyone else missed while they were asleep

None of this is a separate role: a station that can do some of these and not others is an ordinary archiver doing well, and serve:archive says so honestly. On the internet, where stations whose neighbours are all remote are invisible to each other until one archiver they can all address holds the conversation (section 12.12.2), an always-on archiver is where public traffic is pushed and pulled. The same role runs on a satellite or beyond Earth; every hop is store-and-forward, which does not care how long the light takes. Humble stations ask such an archiver what their own gossip does not know, the way section 9.12.1 already falls back to serve:archive.

12.10 Two archivers meet

A station that was away has a hole in its archive as wide as its absence, and the tool that fills it exists: cmd:history (section 11.2). This section adds only WHEN to use it on the archiver's own behalf.

On sighting, an archiver that hears serve:archive from a station back in radio range, or just powered on, asks for the window it missed:

t:command f:X3ARC1 d:X3ARC2 ts:2026-08-20_08:30:00 cmd:history since:2026-08-20_06:12:44 sig:<60 characters>

since: is the newest packet the asker held WHEN THE HOLE BEGAN (at power-on, the newest already on disk), not the live newest, which fresh traffic has already pushed past the missed window. Overlap is harmless: replayed packets deduplicate on the section 5 identifier. The peer re-airs its spool for that window under its own section 30 budget, usually on the bearer the ask arrived on (freshest evidence of a path, section 12.0) but never merely because the code reached for it. The replayed packets are ORIGINALS, each verifying on its own signature; nothing is imported, and both archives converge on the union of what either heard.

Discipline: ask once per peer per absence (a cooldown of at least ten minutes); never ask without a synced clock (a since: from a wrong clock asks for the wrong window); and a station with nothing missing asks for nothing, an empty reply costs the peer its budget all the same.

12.10.1 The pocket device polls

Section 12.10 is for stations that notice an absence. A pocket device cannot: it sleeps in a pocket, wakes on a desk, and has no idea which was an absence worth naming. So it keeps a WATERMARK and polls.

The watermark is a persisted timestamp PER ARCHIVER: the end of the last window that archiver answered for. Not one shared mark, since an archiver holding nothing answers code:404, which advances its mark, so a shared one would let an empty peer narrow the window on a full one standing beside it. The poll is the same ask as any meeting:

165  t:command f:X1QZ3N d:X3ARC1 ts:2026-08-20_09:30:00 scope:local cmd:history since:2026-08-20_09:20:00 sig:<60 characters>

since: is the watermark; scope:local keeps the ask on the short-range bearers, where a locally-reachable archiver is. The cycle:

  1. Every poll period (default TEN MINUTES), the device looks at the stations it heard DIRECTLY (no via:) within the beacon-staleness window and picks those announcing serve:archive.
  2. To each, at most one ask per period: cmd:history since: the watermark. A fresh install has no watermark, asks for nothing, sets it to now and starts keeping.
  3. THAT archiver's watermark advances to the ask's ts: on code:200 or code:404 (nothing held is an answer). It does NOT advance on code:429, on silence, or on code:206, which closes a page rather than the window (section 11.2.1). 3a. An archiver's count: (section 13.0.1) MAY bring the next ask forward (the number moved), but MUST NOT postpone one indefinitely: the period in step 1 is a backstop always armed, because a count: can be absent or stale, and a device that lets a frozen number silence its poller stops fetching while looking healthy.
  4. A single ask is bounded to seven days; a device away longer asks for the last week, and anything older is fetched deliberately, not by a background poll.

Ten minutes is not arbitrary: section 30's reference budget answers a known caller six times an hour. A faster device steals its own budget; a slower one merely learns the news later. Section 12.10.2 is what a station does with this once it can see which of its archivers are busy and which have been silent since spring.

12.10.2 The poll adapts to what it finds

One clock for every archiver is the right default and the wrong steady state: a room silent since spring costs the same metered replay as a live one, and the archiver pays for both. A station SHOULD keep a SEPARATE interval per archiver and move it by what that archiver answers, measured free from the answers already coming back:

the archiver answered the interval
a page with records we did not have halve it, it is talking
code:206, more held than served ask the continuation IMMEDIATELY
code:200 or code:404 with nothing new double it
code:429 double it, and never faster than once a minute afterwards
silence leave it; silence is evidence about the path, not the room

The ceiling is how long that archiver has been quiet, counted from the last time it gave this station something new:

silent for asked at most every
less than a week 10 minutes (12.10.1's period)
a week or more 60 minutes
three months or more 6 hours

The floor is what the peer permits, and it is not the caller's to choose. Section 30.2's reference budget answers a known caller six times an hour, which is why 12.10.1's period is ten minutes:

code:429 is the archiver's authority over the caller's cadence, the one answer that must always slow a station down. Treating it as silence, asking again on the same schedule, being refused again, is a loop that never advances a watermark and looks from outside like a quiet network.

Nobody awake, nobody polled fast. A device whose screen is off falls back to the quiet ladder whatever the room is doing; a fast tier exists to put words on a screen somebody is looking at, and running it into a pocket spends battery and somebody else's budget to no end.

Spread the herd. A station SHOULD jitter each interval by a small fraction (a tenth is enough), or many devices on the same nominal period arrive together and the load an archiver feels is the peak, not the average.

Two properties make all of this safe to get wrong: a replayed packet deduplicates on its section 5 identifier, so an interval too short costs airtime and never correctness; and the watermark only advances on an answer, so one too long delays news and never loses it.

12.11 When the store is full

An archiver's capacity is whatever its operator gave it, and when it is full something must go. Section 30.3 stands: retention belongs to the station. What follows is the DEFAULT the reference stations ship, because a default chosen badly deletes somebody's mail to keep a stranger's chatter. Decided by class first and age second, within a class the oldest goes first, and no packet outlives its own until:.

Discarded first, kept longest last:

  1. The spool, ordinary heard traffic: the bulkiest class and the cheapest to lose, since any peer's spool overlaps it and section 12.10 refills holes.
  2. Custody mail, store-and-forward held for stations that did NOT name this archiver (section 6.3): losing it costs a delivery somebody else may still make.
  3. Declared mail, for callsigns whose t:mailbox hold: names THIS archiver (section 9.12): those recipients chose this station deliberately and check it first, so it is the last thing an archiver may drop, and inside its until: it should never be.

The classes read straight off the packets: d: plus a hold: naming this station is class 3, d: without one is class 2, everything else class 1. An archiver that cannot take new mail without touching class 3 refuses out loud instead, code:429 and, when it can, m:try naming a peer with room (section 30.3).

12.11.1 When the FILE store is full

An archiver that hosts content-addressed files (section 12.9.2, serve:files, cmd:put) keeps a SECOND store, the bytes behind file: references, with its own budget and its own eviction. A quota policy that mentions only the packet spool has skipped the store where the bytes actually are: a picture is kilobytes to megabytes, a packet is at most 250. The order mirrors section 12.11's classes, read off how the bytes came to be held:

  1. Cached bytes a station fetched for its own reading and re-seeds as a convenience: the cheapest to lose, because the reference that names them is still resolvable from any other holder (sections 8.1, 12.9.2).
  2. Deposited bytes a peer pushed with cmd:put and this station accepted as custody: losing one costs a delivery somebody else may still make.
  3. Declared bytes: a file whose recipient declared this station as its mailbox (section 9.12), a file the operator pinned, and the station's own content. The last dropped, and inside its until: it should not be.

Within a class the least recently served goes first, the read count being the signal a packet's ts: is, and no file outlives its own until:. A host that cannot accept a cmd:put without touching class 3 refuses it out loud, code:429, with m:try naming a peer with room, exactly as section 12.11 refuses mail.

The location index follows the bytes. A holder advertises the files it keeps as signed provider records (sections 12.9.2, 12.9.4); when it evicts a file it lets that record expire rather than answering q:have for bytes it no longer holds. A provider record is soft state with a short ttl, refreshed while the bytes are held and gone shortly after they are dropped, so the index never points at an empty store. A reader redirected to a stale holder falls back to the next holder the index names, the same m:try list a miss already carries.

12.12 Reaching a callsign from anywhere

Everything above composes into one sentence: a message handed to any archiver, from anywhere, reaches the callsign it names, wherever that callsign last touched a radio. In the order a message meets them:

  1. Deposit. A sender (a phone on another continent, a server script, a station three bearers away) hands t:message d:X1BOA3 to any archiver it can reach (sections 12.3, 12.7). The archiver holds it: already mail, already bounded by until:, already released by receipt.
  2. Consult. The holder consults its gossip for X1BOA3 (section 12.9.4): the recipient's declaration first, the freshest live sighting second, the visit history third. A holder with nothing asks an always-on archiver.
  3. Forward. The held wire moves as custody toward the declared mailbox and every gateway with a fresh sighting (section 12.8.1), signed by its author, via: growing, loop-checked, once per holder and gateway. It can cross the internet on one hop and a LoRa hill on the next; every hop stores, so nothing is lost to a link down today, and a report that arrives after the mail did moves it then.
  4. Deliver. The gateway that actually hears X1BOA3 releases the mail the moment it does (section 12.8.1), and the receipt (section 9.7) walks back through every holder and clears them all.

Against APRS-IS: an igate here is any archiver with a radio, and unlike an igate it holds mail rather than forwarding into the void; the central servers are replaced by gossip among archivers, with always-on archivers as optional deep memory but no single table anyone must trust or fund. Last-heard routing is APRS-IS's proven idea done with signed claims instead of trusted servers. And absence, which loses an APRS message, merely delays an XPRS one, since every hop stores, at the scale of a pocket or of the solar system.

What this does NOT promise: delivery to a callsign nobody ever heard (a declaration can pre-position its mailbox), secrecy of the envelope (section 12.7), or that any station carry anything against its budgets (section 30.3). The promise is narrower: if the callsign touches the network anywhere its gossip reaches, the mail finds it.

12.12.1 Constrained internet transports

The chain crosses the internet on shared transports run by strangers, with their own budgets. Operating experience is blunt: a shared transport filters, and the subset that reliably crosses is small, identity announcements and directed messages. Broadcast wapp data, bulk announces, anything addressed to nobody may be dropped at any hop, silently. A design that works only on an unfiltered transport does not work.

The two guaranteed lanes: (1) an identity announcement, the transport's own currency, since without paths nothing routes; (2) a message directed at a named destination the transport has a path to. Everything an archiver needs to say across the internet MUST be expressible as one of these, and every rule below is that applied to one exchange:

A shared transport meters announcements, and mutes rather than warns. A transport node admits a bounded rate of announcements per interface and, past a grace, queues a violator's announcements for hours behind everyone else's, with no error to either end. A station that announces on every reconnect, every restart and every operator poke therefore stops being heard while every part of it reports success; measured on the bench, two phones on the same four hubs with mutual paths could not learn each other's address after such a burst. So a station MUST NOT announce more often than the transport's grace allows: the periodic re-announce is the paced one, and a request to announce inside that window collapses into it rather than adding to it.

The well-placed station forwards for its neighbours. A station on mains power with a fixed link, the home server of section 30.3 rather than the phone or the dongle, SHOULD act as a transport node: it rebroadcasts the announcements it hears on its edge bearers into the shared transport, answers path requests for the destinations it knows, forwards the traffic addressed through it, and offers a hub on its own LAN. The stations around it then reach the wider network through one connection under one announce budget, instead of each dialling a stranger's hub and each being metered there, and traffic between two of them never touches a public hub at all. A pocket device on battery or cellular is never volunteered as one, and the promotion ends when the charger does.

Forwarding for everybody does NOT mean airing everybody onto every bearer. A station that takes this role MUST NOT re-air the shared transport's announcement traffic onto a constrained bearer such as a radio others are sharing: it carries what it hears onward on the bearers that can afford it, and lifts its own radio neighbours up into the transport, but the flood stops at the edge of the wire that would drown in it. Two more copies are never made: a relayed announcement is not broadcast onto a bearer with nobody on it, since it discovers nothing and comes straight back; and it is not carried from one shared transport to another by a station that is merely a client of both, since they hold the same flood already and the client pays for every copy.

Answering for others is what makes a transport node one. A station that only forwards is a wire; a transport node also replies when a neighbour asks which way a destination lies, and it replies with the destination's OWN signed announcement, replayed, tagged so the asker addresses the destination through it. A relayer cannot forge that answer, so it must keep what it heard: this is the memory a transport node spends and a leaf does not. The consequence is the point of the role — a station that never heard a destination announce can still reach it, because the station it is attached to did.

A station that only dials out forwards for the whole network, not just its own room. Reachability here is not an addressing property: a path is what announcements taught, hop by hop, keyed on the destination, and a packet travels back along links that are already open. So the stations attached to a transport node become reachable from anywhere the node's rebroadcast announcements travel, whether or not anything can open a fresh connection to it. What dialling decides is only who may start the next connection, never who may be reached over the ones that exist.

And still no inbound ports. Nothing here opens a listener: every reach across the internet is an outbound dial, and a directed message crosses two NATs because the transport routes it, not because either end is reachable. A deployment instruction beginning "forward port" has misread this document. What is NOT constrained: a transport one operator controls end to end (a private hub, a LAN, a point-to-point link) carries whatever its operator pleases, and the broadcast forms elsewhere remain correct there.

12.12.2 Public traffic across the internet: the archiver is the meeting place

Section 12.12 routes mail to ONE callsign. This is the other half: public traffic, the broadcast addressed to everybody, does not cross the internet by being broadcast. It crosses because archivers hold it and others ask them for it.

The failure is quiet and every part of it reports success: two stations on different networks hear each other's announcements all day, each publishes a t:message with no d:, the local bearers report sent, and neither ever sees a word the other said. Why: a broadcast is an announcement, and shared transports do not cross-forward announcements between their clients (section 12.12.1). The answer is the archiver, as both destination and source:

Addressing is a prerequisite, and it fails silently. All of the above is directed traffic, so it depends on turning a callsign into the transport's own address, and when that lookup fails an implementation naturally falls back to broadcasting, the fallback being the failure dressed as a send. So a station SHOULD publish its callsign as the display name on whatever identity the transport announces; and a resolver MUST read every name the transport offers, not the first it parses, since callsign-to-address for sending is the direction that fails silently and so the one to check against the air.

A station too small for a link layer still has a directed lane. "Directed" means addressed to one destination, not any particular mechanism. A microcontroller with kilobytes free cannot carry a link layer or a store-and-forward router and does not need one: an addressed, encrypted single packet reaches a named destination through the same transports, forwarded by destination like anything else, measured multi-hop across a public hub between two networks. What it gives up is the receipt: silence and refusal look alike, the right trade for an ask that repeats anyway (a poll, a gossip query, a replay request, section 12.10.2), the wrong one for mail, which is why mail is held under custody (section 12.7). So a constrained station's obligations shrink to three: announce so paths exist, answer where it was asked from, and never assume its packet arrived.

Send on every lane that might work. Where a transport offers more than one addressed mechanism, a directed wire MAY go out on all of them: they fail independently, and section 5's identifier collapses the duplicate into one record on arrival. Measured on the bench: two phones on different networks exchanged one addressed form while the other stayed silent between the same endpoints.

The whole of this section reduces to one sentence: on a transport nobody owns, an archiver is not an optimisation of public traffic, it is the mechanism.

12.12.3 A station as a transport node

This section is architecture, not vocabulary: nothing in it appears on the wire in XPRS terms. It exists because the network XPRS rides across the internet, Reticulum, has a role in it that an XPRS station can take, and taking it well is what makes sections 12.12.1 and 7.7.6 work in practice. Reticulum distinguishes an instance, which sends and receives for itself, from a transport node, which also forwards for others: it re-airs the announcements it hears so paths form beyond its neighbours, answers a neighbour's path request out of its own table, and carries packets addressed through it hop by hop. Reachability in this network is not an addressing property. A path is what announcements taught, keyed on the destination; a packet returns over links that are already open; and a station that only ever dialled out is a complete transport node for everything attached to it. Whether it can be dialled decides who may open the next connection, never who can be reached.

Every XPRS station is at least a leaf with an edge bridge. A phone dials the public hubs it knows and, when it has a radio, lifts the announcements of its radio neighbours up into the transport, so a Bluetooth-only station a metre away becomes reachable from the other side of the world. It does not carry the hubs' announcement flood the other way: that radio is shared, and drowning it is the one thing the bridge exists to prevent.

A station on mains power with a fixed link is promoted to a transport node, on the same measure that makes it an always-on archiver (section 12.9.4): the home server, the phone on its charger on the household Wi-Fi. Promoted, it

And four things it never does, each learned by doing it. It never re-uploads an announcement heard on one shared hub to another: the hubs hold the same flood already and a client pays for every copy. It never broadcasts a relayed announcement onto a bearer with nobody on it: it discovers nothing and comes straight back. It never learns from its own frame coming back, since a path whose next hop is oneself is not a path. And it never announces faster than a hub's grace allows, because a hub mutes an over-announcer for hours and says nothing, so a station that re-announces on every reconnect stops being heard while every part of it reports success.

What this buys the packet lane. Section 7.7.6 sends a file as a stream of single packets that never open a link. Such a packet crosses a shared hub exactly because a transport node forwards single packets addressed through it, and it lands only where a path exists. A promoted station's neighbours have paths they never had to learn themselves; and the station itself, answering path requests, keeps those paths from expiring into the silence a stream of small packets cannot afford. Measured, both phones on public hubs and different networks: a 20 kB file in 219 packets in 43 seconds, none lost.

Demotion is the same measure reversed: unplugged, or on cellular, the station is a leaf with an edge bridge again, and its LAN hub closes. A pocket device is never volunteered as a transport node, however capable, because what the role costs is not CPU but a radio and a battery somebody else is relying on.


13. Services

t:service says what a station does for other stations.

t:service f:X3RLY7 pos:38.7810,-9.2043 serve:relay,archive ts:2026-08-08_14:26:40 sig:<60 characters>

146 bytes: a node that repeats packets and carries mail.

serve: is a comma-separated list from a fixed set:

Word The station
relay repeats packets it hears
archive an archiver (section 12): keeps a spool of what it hears and re-airs it on cmd:history, archives its depositors' publications and answers queries, holds mail for stations that named it (or for anyone, as it pleases), and publishes its directory of who-keeps-what (section 12.9). One word covers all of it, there is no separate service for the pointer half or the storage half, and the offer is the same on every bearer the station has (section 12.0)
internet gateways to the internet
aprs gateways to APRS-IS
meshtastic gateways to a Meshtastic mesh on its LoRa channel (sections 3.2, 9.11.5)
nostr runs a NOSTR relay
files hosts content-addressed files: answers q:have (section 8.1) and cmd:file, and accepts cmd:put deposits within its budgets (section 11.2)
devices operates automated devices, each an X4 station of its own (section 11.7.1)
time has a clock worth trusting, usually from GNSS
weather publishes observations
wifi offers network access to people nearby
other something not in this list, described in m:

The set is fixed on purpose, and a receiver ignores a serve: word it does not know rather than dropping the packet or acting on the word. This list is what every implementation can act on; anything else belongs in m: behind other. A word one implementation invents is worse than no word: this project's app and firmware both aired serve:archive,super for a while to mean "always on", which no other build could read, while 12.9.4's qualities -- already on the same beacon, in count: and uptime: -- said it in a way anybody could. Both now air archive alone, and an old station's second word parses away to nothing.

13.0.1 count: on an archiver's announcement

An archiver states how much it is holding:

117  t:service f:X3P7QK serve:archive count:1234 fw:1.4.2 sig:<60 characters>

count: is the number of RECORDS the archive holds, not the number of callsigns it has heard from. The distinction is the whole value of the field.

A listener uses count: to decide whether asking is worth it. Section 12.10.1's pocket device cannot afford a cmd:history per period on the chance that something arrived, a replay is metered (section 22.2), and one spent to be told "nothing changed" is one not available when something did. So it remembers the last count: it saw from each archiver and asks only when the number moves.

Counting callsigns breaks that, and breaks it silently. An archiver that has heard from six stations for a month reports count:6 however much those six say, so a listener watching it sees a number that never changes and concludes there is nothing to fetch, while the archive fills up behind it. The failure looks like a working poller, which is the worst kind.

A record count is not required to be exact, and an archiver that evicts under section 12.11 will see it fall. What is required is that it MOVES when the archive takes something new, because that movement is the signal. An archiver that cannot cheaply produce one omits count: rather than publishing a constant: a listener that sees no count: falls back to asking on a period, which is slower but honest, and one that sees a frozen number does not.

A listener must not treat count: as knowledge that survives. It is a reading, and readings go stale, particularly across a change of bearer, where the announcement carrying it may not travel. Stale knowledge is worse than none because it looks like knowledge, so count: may only ever bring an ask FORWARD; it may never be the reason one is skipped indefinitely. Section 12.10.1's period remains the backstop whatever count: says or fails to say.

(count: also appears on t:file kind:folder, where it is the number of files in a listing, section 7.7.3. The packet type says which is meant.)

A station with a position and a power source says so, because both decide whether it is worth routing through:

t:service f:X3RLY7 pos:38.7810,-9.2043 serve:relay,archive,internet,aprs source:solar ts:2026-08-08_14:26:40 sig:<60 characters>

173 bytes. source:solar from section 14.3 means it survives a power cut, which is when a gateway matters most.

13.1 What this is not

Physical goods and help are t:offer, not this. Water, fuel, shelter, a lift, a spare battery and a berth are already in section 27 with a price and an expiry, and they belong to a person rather than a station. t:service is what a radio does on the network, and the division is worth keeping: a station offering internet is advertising a route for packets, and one offering wifi is advertising a socket for humans.

13.2 The other half of a mailbox

serve:archive is a station volunteering to hold mail. t:mailbox (section 9.12) is a recipient nominating. They are opposite directions of the same arrangement and neither implies the other.

A sender with mail for an unreachable station looks for a t:mailbox from that station first, because the recipient knows best who sees them. Failing that, any station advertising serve:archive is a reasonable guess. Neither is a promise. A carrier is under the quota and eviction rules of store-and-forward.md whatever it advertised, and a station that stops carrying does not owe anybody a withdrawal.

13.3 Trust

Sign it. Signing is the default (section 7.1) and an unsigned service advertisement is worth nothing: serve:internet is an invitation to route traffic through a station, and forging one is the cheapest way to collect other people's packets.

Even signed, an advertisement is a claim about capability, not a promise of behaviour, and never evidence of good faith. A station that truthfully gateways to the internet may also log everything that passes. Encrypt what should not be read (section 7.2) and set scope: on what should not travel (section 9.11); neither depends on trusting the station that carries it.

13.4 One port on an IP network

A station reachable over TCP listens on port 4242, and that one port speaks both Reticulum and XPRS. 4242 is already the port Reticulum hubs answer on, so an operator opening a firewall for one has opened it for both, and a client guessing the port guesses right.

The two are told apart by the first byte of the connection, and nothing else:

A listener reads one byte and knows which protocol it has. A stock Reticulum client connecting to the port works untouched; an XPRS client sends packets as lines of text, one packet per line, and receives the same. Everything on such a connection is an ordinary packet, a t:ping is answered with a t:pong, a cmd:history with the replay of section 11.2, so the socket adds no new vocabulary, only a wire.

Two rules keep the demultiplexing honest, and they are load-bearing:

The bearer is decided by where the peer actually is, because a TCP socket does not say. A connection from a private or link-local address is the LAN it looks like: recorded under lan, visible on the air view, archived like any local bearer. A connection from a public address travelled the internet whatever port it used, and it is treated like the Reticulum lane: never shown as an air sighting, and archived only under the mailbox-declaration rule of section 34.3.

The same number carries the broadcast bearer, on UDP. TCP needs an address, and the first station on a network knows nobody's. So link:lan (section 15.6) is UDP 4242, broadcast, one packet per datagram, verbatim and with no header of its own, a station joins by opening a socket and is found without being looked for. A packet is at most 250 bytes, so it always fits one datagram and is never fragmented; a datagram that does not parse as a packet is dropped.

UDP 4242 and TCP 4242 are different sockets and never collide. Reticulum's own LAN discovery is a separate protocol on a separate port and is not this.

Broadcast is heard by everyone at once, so relaying on it obeys section 6.2.1 without exception: a station that did not compose the packet waits 200-1200 ms, and drops its copy if it hears the same section 5 identifier meanwhile. Its own packets go out immediately, with no via:.

until: bounds the claim, and it should be short. A service list is a statement about equipment that is switched on, and equipment gets switched off.


14. Channels

t:channel announces a frequency a station uses: what it is, how it is modulated, whether the station transmits there, and when it is listening.

t:channel f:X1QZ3N freq:145.500MHz mode:fm kind:listen since:2026-08-08_18:00:00 until:2026-08-08_22:00:00 ts:2026-08-08_14:26:40

129 bytes: monitoring two metres this evening, receive only.

Key Meaning
freq: the frequency to tune to hear this station
ch: its number in a band plan, where it has one
mode: how it is modulated
bw: bandwidth, where the mode does not imply it
shift: repeater input, as an offset from freq:
input: repeater input, stated outright (section 14.4)
tone: access tone
power: transmit power
range: how far the operator expects it to reach
kind: what the channel is for
pos: where the station or repeater is
site: whether it stays there
source: what powers it
every:, for:, at: a recurring listening window (section 21.2)
since:, until: when the whole schedule starts and stops
m: anything else

kind: takes one of listen, simplex, repeater, beacon, net, gateway, emergency, other.

mode: takes one of fm, am, usb, lsb, cw, ssb, packet, aprs, lora, ft8, psk31, rtty, dmr, dstar, c4fm, m17, dv, other.

14.1 Listening, or transmitting

power: present means the station transmits on that channel. Absent means it only listens. There is no separate flag, because a transmit power is the thing a listener would have had to state anyway, and a station that will not say its power has not told you it transmits.

t:channel f:X1QZ3N freq:145.500MHz mode:fm power:25W kind:simplex ts:2026-08-08_14:26:40

88 bytes.

since: and until: are the listening window, and mean what they mean everywhere else (section 11.1). Their absence says the station listens whenever it is on, not that it never listens.

A recurring schedule is not expressed here. A weekly net is a t:event (section 26) that names the frequency, and this packet describes the channel itself rather than the calendar around it.

14.2 Recurring windows

Three keys describe a schedule that repeats.

Key Meaning
every: how long between windows
for: how long each window lasts
at: the time of day the cycle is anchored to, UTC

The 3-3-3 plan is channel 3, for 3 minutes, every 3 hours:

t:channel f:X1QZ3N freq:446.03125MHz ch:3 mode:fm every:3h for:3min kind:listen ts:2026-08-08_14:26:40

102 bytes.

at: defaults to 00:00:00, so every:3h alone means 00:00, 03:00, 06:00 and so on in UTC, which is what makes the plan work: every station calculates the same windows without anyone coordinating. A schedule anchored to local time would put two neighbours an hour apart on different minutes.

Give at: when the cycle is not anchored to midnight:

t:channel f:CT1ABC freq:14.300MHz mode:usb every:1day at:20:00:00 for:1h power:100W kind:net ts:2026-08-08_14:26:40

115 bytes: every day at eight in the evening, for an hour.

since: and until: bound the schedule itself, and are a different thing from the windows inside it: since: says when the arrangement begins, until: when it lapses. A net that runs weekly through the summer has both.

Absent every:, there is no schedule. since: and until: alone are a single window, and neither means the station is deaf the rest of the time.

14.3 Where the station is, and whether it stays

t:channel f:X3RLY7 pos:38.7810,-9.2043 freq:145.600MHz mode:fm shift:-600kHz tone:123.0Hz power:50W range:40km site:fixed source:solar kind:repeater ts:2026-08-08_14:26:40

171 bytes: a solar repeater on a hill, reaching about 40 km.

site: takes one of fixed, mobile, portable, temporary. It answers a question type: does not: whether the channel will still be there tomorrow. A repeater is fixed, a handheld carried up a hill is portable, a vessel is mobile, and a set installed for a weekend is temporary.

source: says what powers the station, with one word from this list. It is what tells a reader whether a station survives a power cut, which is the moment its frequency matters most. A solar repeater is reachable after the grid drops and a grid one is not.

Word Energy source
grid the public grid, or shore power
battery a battery, charged elsewhere
solar photovoltaic panels or a solar thermal plant
wind wind turbines
hydro rivers, dams and micro-hydro
marine tidal and wave
geothermal geothermal
biomass wood, biogas and biofuel
diesel diesel engines
gasoline gasoline or petrol engines
natgas natural gas, including combined heat and power units
lpg propane and butane
hydrogen fuel cells
nuclear nuclear
fuel a fuel not listed
generator an engine generator whose fuel is not stated
mixed several sources together
other anything else, named in m:

There is no word gas. To one reader it is petrol and to the next natural gas, and a word that means two fuels names neither.

The same list says where a site's production comes from, beside produces: (section 15.5.2).

range: is the operator's own estimate of usable range, as a radius from pos:.

It is an estimate and the document says so. Terrain, weather and the other station's antenna decide what actually happens, and a hill between two stations 30 km apart beats a range:40km every time. It is published because the person who installed the antenna knows better than anyone else what it usually does, and a reader 200 km away can rule the channel out without trying.

t:channel f:X1BOA3 pos:38.6902,-9.4012 freq:156.800MHz ch:16 mode:fm power:25W range:15km site:mobile source:battery kind:emergency ts:2026-08-08_14:26:40

154 bytes: a vessel on channel 16, battery powered, about 15 km on a good day.

14.4 Repeaters that listen elsewhere

freq: is the frequency to tune to hear this station. On a simplex or listen-only channel that is the whole story. A repeater has a second frequency: the one it listens on, which is the one a user transmits on.

shift: gives it as an offset, which is how repeaters are conventionally listed and is shorter:

shift:-600kHz     the input is 600 kHz below the output

input: gives it outright, for when it is not a simple offset:

t:channel f:X3RLY7 pos:38.7810,-9.2043 freq:145.750MHz input:433.000MHz mode:fm tone:123.0Hz power:25W range:30km kind:repeater ts:2026-08-08_14:26:40

150 bytes: listens on 70 centimetres, transmits on 2 metres. No offset can express that, because the two frequencies are not in the same band and the number would be larger than either.

The two forms describe the same thing and a packet carries one, not both:

t:channel f:X3RLY7 freq:145.600MHz input:145.000MHz mode:fm kind:repeater ts:2026-08-08_14:26:40

96 bytes, which is shift:-600kHz written out. Where a station sends both anyway, input: is authoritative, being the measurement rather than the arithmetic.

If the input differs by more than its frequency, a different mode, a different bandwidth, a gateway that hears DMR and speaks FM, it is not one channel with two frequencies. Send two t:channel packets with kind:gateway, one for each side, and let each carry its own mode: and bw:. Cramming a second mode into this packet would mean a second mode: key, and a key appears once.

14.5 One channel per packet

A station that uses several frequencies sends several packets, one each.

This is not a limitation worked around. A key appears at most once in a packet, so two frequencies would need either freq1: and freq2:, or one value packing frequency, mode and power together in a fixed order. The second is how APRS encodes almost everything and the reason a receiver there cannot skip a field it does not understand.

One packet each also means a station adding a band re-sends one packet rather than all of them, a receiver can filter on mode:lora without parsing the others, and a repeater's entry stays correct when the operator's handheld changes.

t:channel f:X3RLY7 pos:38.7810,-9.2043 freq:145.600MHz mode:fm shift:-600kHz tone:123.0Hz power:50W kind:repeater ts:2026-08-08_14:26:40
t:channel f:X3RLY7 freq:433.775MHz mode:lora bw:125kHz power:22dBm kind:gateway ts:2026-08-08_14:26:40

136 and 102 bytes: a two-metre repeater with its offset and access tone, and a LoRa gateway whose power is quoted in dBm because that is how the module is specified.

t:channel f:CT1ABC freq:14.300MHz mode:usb power:100W kind:net since:2026-08-11_20:00:00 until:2026-08-11_21:00:00 ts:2026-08-08_14:26:40 m:maritime mobile net
t:channel f:X3RLY7 freq:156.800MHz mode:fm kind:emergency ts:2026-08-08_14:26:40 m:channel 16, monitored continuously

159 and 117 bytes.

14.6 Transmitting is regulated

A t:channel packet says what a station does; it does not make it lawful. A frequency, a power and a mode together describe a transmission that in most of the world requires a licence, an allocation, or both, and neither this document nor a receiver can tell whether the sender holds one. Announcing a channel is not a claim of authority to use it, and section 7.4 continues to govern what may be transmitted where.

14.7 Meeting on a working channel

Radio settled this long ago: everyone monitors a calling channel, and a pair with real business moves off it. A file transfer at calling-channel rates is minutes of a jammed commons; the move costs one packet there and puts the minutes somewhere private. This section is that move, in vocabulary the document already has.

A t:channel with d: is an invitation: this channel, for you, now. Every field keeps its section 14 meaning, the keys say where, until: says how long the inviter will wait there, q:ack asks for the answer, and r: names the exchange the move serves, usually the cmd:file or cmd:put that made a working channel worth having.

192  t:channel f:X1QZ3N d:X1RD89 freq:433.900MHz mode:lora bw:250kHz until:2026-08-17_16:20:00 q:ack r:b47210 ts:2026-08-17_16:00:00 sig:<60 characters>

When the meeting place is a technology rather than a frequency, link: (section 15.6.1's bearer word) names it and ch: carries whatever label that technology needs, a WiFi channel number, a network name:

164  t:channel f:X1QZ3N d:X1RD89 link:espnow ch:6 until:2026-08-17_16:10:00 q:ack ts:2026-08-17_16:00:00 sig:<60 characters>

The invitee answers the ordinary way, s:ack is "moving now", s:no is "cannot", with the reason where reasons go:

107  t:receipt f:X1RD89 d:X1QZ3N r:72fe2f s:ack sig:<60 characters>
127  t:receipt f:X1RD89 d:X1QZ3N r:d8b7be s:no sig:<60 characters> m:no espnow hardware

The choreography, step by step, because a device listens ONE frequency at a time, and a step out of order strands somebody on a channel nobody else is tuned to. Implementations follow this sequence and expect it of each other:

  1. Invite, on the calling channel. The inviter keeps listening there, it has promised nothing yet and moves nowhere until somebody accepts.
  2. Accept, on the calling channel. The acceptance is the commitment. A s:no, or silence until the invitation's freshness runs out, ends the matter with everyone still on the commons.
  3. Move. On HEARING the acceptance the inviter tunes to the working channel and listens; on SENDING it the invitee tunes and follows. From this moment both are deaf to the calling channel, which the rest of the network handles as ordinary absence: anything addressed to them waits in custody or a mailbox like mail for any station that is away (section 6.3).
  4. "I am also here, start sending." The invitee re-airs its acceptance ON the working channel: the same signed packet, the same identifier, and hearing it there is the proof it cannot fake from anywhere else, the party is tuned, present, and ready. The station with the bulk to transmit sends nothing until it hears this. The same packet serves twice: the first airing commits, the second locates.
  5. Work, then give the channel back. The exchange runs (section 11.2.2's middle block); when it ends, or until: passes, whichever is first, everyone returns to the calling channel. A transfer the working channel killed mid-way is resumed later with off: (section 11.2) on whatever lane the pair next shares; the commons is not the place to debug it.
  6. Nobody came. An inviter alone on the working channel at until: returns to the commons and treats the acceptance as overtaken by events, no error packet, because the party that failed to arrive is not listening anywhere useful to send one.

More than two parties works the same way, because every step is already per-station: the invitation goes to a group (d: takes a group name), each member accepts or declines individually on the commons, and the transmitting station starts when the parties it heard arrive, step 4, once per accepting station, or when until: forces its hand. Whoever accepted but never arrived catches up like any absent station: cmd:history, a mailbox, the next snapshot.

Rules, all inherited:

This is also the missing handshake of section 11.2.2: when a pair's best bulk lane is not obvious from the bearers they are already on, the invitation is how one proposes and the other agrees, and the transfer's control packets then bracket a lane both actually chose.

14.8 LoRa modes

A LoRa receiver hears only the modulation it is set to, down to the sync word the radio compares before any software sees a byte, so two networks meet on a channel only by agreeing on it. Which LoRa network a station shares its channel with is therefore a setting, its LoRa mode, chosen by the operator (lora:, section 11.10) and taken at once:

Mode The channel On it
xprs XPRS's own: spreading factor 7, 125 kHz, coding rate 4/5, an 8-symbol preamble, sync word 0x12; 869.5 MHz in Europe (868.2 MHz in band g1), 903.9 MHz in the US, 917.0 MHz in Australia XPRS packets, one per frame, as on every other bearer
meshtastic Meshtastic's default, "LongFast": spreading factor 11, 250 kHz, coding rate 4/5, a 16-symbol preamble, sync word 0x2B, on Meshtastic's frequency for the region, 869.525 MHz in Europe XPRS inside Meshtastic frames, and Meshtastic relayed and translated
meshcore MeshCore's default, measured off a node of theirs: spreading factor 8, 62.5 kHz, coding rate 4/5, a 16-symbol preamble, sync word 0x12, 869.618 MHz in Europe XPRS inside MeshCore frames, and MeshCore relayed and translated

Every mode carries channel presets, and the channel is a setting of its own: the same radio is sold matched for 433, 868 and 915 MHz, so region: takes a preset and freq: an exact channel (section 11.10), both at once and both remembered. A station says which it is on in its answers and in its t:channel beacon.

Stations in different modes do not hear each other on LoRa, and still meet on every other bearer: the mode decides who shares the radio, not who is on the network. A station changes mode without restarting, so it may also be asked to LISTEN on each mode in turn before its operator chooses: which networks are within reach is a measurement, not a guess.

In meshtastic mode:

114  t:channel f:X3DCK0 freq:869.525MHz mode:lora bw:250kHz ch:longfast power:14dBm kind:gateway ts:2026-09-19_12:00:00
141  t:service f:X3DCK0 serve:archive,meshtastic count:212 ts:2026-09-19_12:00:00 sig:<60 characters>

An XPRS packet is the payload of a Meshtastic frame on a private port and a channel of its own, which every Meshtastic node ignores and relays. A station also relays Meshtastic's own traffic by Meshtastic's rules, and a gateway translates between the two (section 9.11.5); it says meshtastic in its serve: only in this mode.

In meshcore mode:

112  t:channel f:X3DCK0 freq:869.525MHz mode:lora bw:250kHz ch:public power:14dBm kind:gateway ts:2026-09-20_12:00:00
139  t:service f:X3DCK0 serve:archive,meshcore count:212 ts:2026-09-20_12:00:00 sig:<60 characters>

An XPRS packet is the payload of a MeshCore frame of the type MeshCore keeps for payloads a node does not understand, flood-routed. A MeshCore repeater HEARS it and does not carry it, so XPRS on that channel reaches the stations in radio range and no further; what crosses the whole of MeshCore is what the gateway translates. A station also relays MeshCore's own traffic by MeshCore's rules, and a gateway translates between the two (section 9.11.5); it says meshcore in its serve: only in this mode.

MeshCore asks its users nothing about the internet, and its public channel is one valley's air like Meshtastic's (section 9.11.5), so everything translated from it arrives scope:regional: the air carries it, the Regional room shows it, and no gateway publishes it. Its channel messages are not signed and name their sender only by a name, so one crosses only under the address of a node whose advert the gateway heard carrying that name; a name nobody has claimed is not an address and the message stays where it was said.

MeshCore addresses a node by the first byte of its public key and its users carry each other's keys, so an XPRS callsign is reachable there only once a gateway has advertised it: the callsign's MeshCore identity is derived from the callsign itself, so every gateway advertises the same one and any of them can carry that callsign's mail. Nothing about that identity is secret, and a message that crosses is in the clear on the other side, which is the same bargain as section 9.11.5's.

Two consequences for the airtime of section 30. A full frame is about two seconds on the air, five times what the same packet costs in xprs mode, on a channel two networks share. So a station puts on LoRa only what somebody is waiting for, messages, receipts, calls for help, commands and their results, keys to verify them, and leaves presence (beacons, service announcements) to the bearers that are cheap. In xprs mode, on a channel of its own, it carries everything. In every mode a station listens before it talks.


15. Observations

Position, movement, weather and telemetry share one packet type and one vocabulary. There is no separate weather packet and no separate telemetry packet. A weather station is a station that reports temperature in addition to position.

An observation is a broadcast by default. It carries no d:, is addressed to nobody, and every station that hears it may read it, show it and use it. That is the default because a reading is a fact about a place or a station, and the value of a fact grows with the number of stations that have it: a temperature, a position or a solar yield heard by a whole street is worth more than the same packet delivered to one of its houses. An answer to a request is no exception (section 8).

A station may address an observation with d: when the reading concerns one station alone: a lock answering its owner (section 11.7), a doorbell telling its owner someone is at the door, a mailbox telling one callsign how much it holds for it (section 9.12.3). d: says who the reading is for; it does not hide it from anyone else in range. A reading that must stay private is sealed (section 6.2), not addressed.

15.1 Position

pos: is decimal degrees, WGS84, latitude then longitude, negative for south and west. No hemisphere letters, no degrees-minutes-seconds, no compression.

p:38.7223,-9.1393

The number of decimal places states the precision claimed:

Decimals Resolution Appropriate for
2 1.1 km a town
3 110 m a district
4 11 m a normal satellite fix
5 1.1 m a good fix or survey equipment
6 0.11 m rarely justified

A station sends only the digits its fix supports, and reports measured uncertainty separately in acc:.

Absence of pos: means the position is unknown. It does not mean zero. 0,0 is a valid coordinate in the Gulf of Guinea.

15.2 Movement

Key Type Meaning Quantity
pos coord position degrees
alt qty altitude above mean sea level distance
acc qty horizontal accuracy radius distance
spd qty speed over ground speed
dir qty course over ground, the direction it is travelling angle
o qty heading, the direction it is pointing angle
climb qty vertical speed, signed speed

dir: and o: are different measurements and a station may report both. dir: is where it is going, which is what a satellite fix gives. o: is where it is pointing, which is what a compass gives. They agree on a road and disagree wherever wind or current pushes a vehicle sideways:

t:observation f:X1BOA3 pos:38.6902,-9.4012 spd:6kt dir:275deg o:262degm type:boat ts:2026-08-08_14:26:40

104 bytes: making 6 knots over the ground towards 275 true, with the bow held at 262 magnetic to hold that track against the current.

A station with only a satellite fix sends dir: alone, which is the common case. A station that is stationary has no course and may still have a heading:

t:observation f:X1CAR7 pos:38.7231,-9.1402 o:212degm type:car ts:2026-08-08_14:26:40

84 bytes.

15.3 Weather

Key Type Meaning Quantity
temp qty air temperature, outdoors temperature
hum qty relative humidity, outdoors proportion
intemp qty air temperature, indoors temperature
inhum qty relative humidity, indoors proportion
press qty barometric pressure, station level pressure
wind qty wind speed, sustained speed
wdir qty wind direction, the direction it blows from angle
gust qty wind gust, peak speed
rain1 qty rainfall, previous hour rainfall
rain24 qty rainfall, previous 24 hours rainfall
solar qty solar irradiance irradiance

A station reports in the unit it works in and says which it is (section 15.8). A station holding Fahrenheit sends temp:57.6F; it does not convert, and the receiver does.

temp: and hum: are outdoors. intemp: and inhum: are indoors. A key beginning with in is the indoor counterpart of the key that follows it.

They are separate keys rather than one key with a flag saying where the sensor sat, because a station commonly has both and would otherwise need two packets to report what it measured at one moment:

t:observation f:X3WX01 pos:38.7223,-9.1393 temp:14.2C hum:78% intemp:21.5C inhum:54% press:1013.2hPa type:wx ts:2026-08-08_14:26:40

131 bytes: 21.2 outside, 21.5 in the room, one timestamp, one transmission.

A station with only an indoor sensor sends only the indoor keys, and the reading is no longer mistaken for outside air:

t:observation f:X3WX01 intemp:21.5C inhum:54% age:60

52 bytes.

A station with one sensor that does not know where it sits sends temp:. Outdoors is the default because it is the reading another station can use: an outdoor temperature describes the air a neighbour is standing in, an indoor one describes a room only its owner cares about.

Pressure has no indoor form. The difference across a wall is smaller than the instrument's error, and press: is already defined at station level.

15.4 At sea

Key Type Meaning Quantity
wave qty significant wave height distance
swell qty swell period duration
seatemp qty sea surface temperature temperature
vis qty horizontal visibility distance
t:observation f:X1BOA3 pos:38.6902,-9.4012 wave:1.8m swell:9s seatemp:18.4C vis:2km wind:11m/s type:sailboat ts:2026-08-08_14:26:40

131 bytes.

These decide whether a passage happens, and nothing else in the format carries them. Wind and pressure describe the air; a two-metre swell at nine seconds is a different sea from a two-metre swell at four, and the number that tells them apart is the period.

vis: is a distance rather than a category, so vis:200m in fog and vis:20km on a clear day are the same measurement rather than two vocabularies.

A vessel sends what its instruments give it and omits the rest:

t:observation f:X1BOA3 pos:38.6902,-9.4012 wave:1.8m seatemp:18.4C type:boat ts:2026-08-08_14:26:40

99 bytes.

15.5 Telemetry and station type

Key Type Meaning Quantity
batt qty battery charge proportion
volt qty supply voltage voltage
link enum which bearer a reading is about (section 15.6)
busy qty proportion of the last hour that bearer was occupied (section 15.6) proportion
txtime qty proportion of the last hour this station transmitted proportion
hears path callsigns heard directly, most relevant first (section 15.6.3)
peers int how many stations are reachable in total (section 15.6.4)
mail int messages held for other stations (section 15.6.5)
rssi qty received signal strength signal power
snr qty signal-to-noise ratio signal ratio
uptime qty how long the station has run without interruption duration
lifetime qty how long the station has run in total, across every restart duration
odometer qty distance travelled over the station's service life distance
type enum what the station is or is riding on, from the set in section 21.2
fw text the firmware version this station is running (section 11.8)
state enum a device's principal condition, from the closed list in section 11.7
level qty how far, when a condition is partial (section 11.7) proportion
target qty the setpoint a device holds a reading at (section 11.7)

Radiation readings, ionizing and electromagnetic, are their own family, section 15.5.1.

rssi and snr describe the radio path a packet arrived on and are reported by the receiver, in a pong reply. A station does not transmit its own received signal strength.

uptime and lifetime are the station's account of its own stability, for whoever is deciding whether to route through it or nominate it a mailbox.

t:observation f:X3RLY7 link:ble peers:4 mail:3 uptime:26h lifetime:38day

72 bytes. uptime resets to zero at every restart, on purpose: a station that reboots hourly cannot claim otherwise for long. lifetime is accumulated service time, the sum of every period the station has been running since it first kept records, so it survives restarts and needs no wall clock, which a station reporting epoch: (section 15.7) does not have. It is not the calendar age of the hardware: a dongle powered one hour a day for a year reports lifetime:15day, and that is the honest figure for a station whose value is being on the air.

Both are claims, not measurements a receiver can check. Like serve: (section 9.3), they say what the sender believes about itself; a receiver weighs them against what it has observed. Coarse figures are the expected form, uptime:26h rather than uptime:94340s, because the reading changes by the second while its meaning changes by the hour, and the bytes are better spent elsewhere.

A key beginning with life accumulates since the station's records began, across every restart and without a wall clock. lifetime: is the running time, lifedose: the absorbed dose (section 15.5.1), and the energy counters of section 15.5.2 the energy. A new accumulating reading takes the same prefix in front of its reading's own name, lifeproduces: for produces:. A reading that flows both ways takes one counter per direction, named for the site's side of it: lifegridin: for what came in from the grid and lifegridout: for what went out.

odometer: is the moving station's counterpart to lifetime:: how far it has travelled over its service life, in any distance unit (section 15.9). The word is the instrument's own, so a reader needs no glossary.

t:observation f:X1SHIP pos:38.7012,-9.1523 spd:12kt odometer:15420nmi lifetime:210day ts:2026-08-13_10:00:00

108 bytes: a ship with 15,420 nautical miles behind it across a 210-day service record. A car reports odometer:48213km. Like lifetime: it is the station's own account, a receiver weighs it, not verifies it.

An observation carries a note in m:, the same key a message uses.

15.5.1 Radiation

Radiation a station can measure and a person would want to know about: ionizing radiation from nuclear sources, and the electromagnetic fields of transmitters and power systems. One family, one discipline, built to grow.

Key Type Meaning Quantity
dose qty ambient ionizing dose rate, what a Geiger counter shows dose rate
lifedose qty ionizing dose accumulated since the station's records began dose
radon qty radon activity concentration in the air activity concentration
rf qty radio-frequency power density power density
efield qty electric field strength electric field
mfield qty magnetic flux density magnetic flux density
t:observation f:X3WX01 pos:38.7223,-9.1393 dose:0.14uSv/h ts:2026-08-13_10:00:00
t:observation f:X3WX01 dose:0.14uSv/h lifedose:1.2mSv lifetime:38day ts:2026-08-13_10:00:00
t:observation f:X3WX01 rf:120uW/m2 efield:1.8V/m mfield:0.3uT ts:2026-08-13_10:00:00
t:observation f:X3LAB1 radon:120Bq/m3 ts:2026-08-13_10:00:00

80, 91, 84 and 60 bytes. Normal background dose rate is roughly 0.1uSv/h to 0.3uSv/h, so dose:2.5uSv/h reads as anomalous on sight, which is the point of putting the unit on the value rather than in a manual.

These are instrument readings, never health claims. A receiver may plot them, compare them against its own idea of background, and decide what it thinks; the sender asserts only what its instrument showed. And an absent key means not measured, never safe, the same rule cw: states in section 4.6, for the same reason.

lifedose pairs with lifetime: exactly as it reads: everything the station absorbed over the service record it is already reporting. dose is the now, lifedose is the history.

A telemetry stream is not an alarm. A station watching a sustained anomaly raises t:warning kind:radiation (section 24), which carries the nine-relay budget an emergency deserves; the observation stream keeps its ordinary three and its ordinary cadence (section 22.1).

Counts per minute are refused. A CPM figure is a property of the tube that produced it and means nothing without out-of-band calibration, which design rule 6 forbids. A station converts to dose rate before transmitting, using its own tube's factor, the one party that reliably knows it.

Growing the family costs what section 4.9 says and nothing more: a new reading takes a new key in this table and its unit family in section 15.9 with a canonical unit, no new packet type, no version, no negotiation. A reading not yet adopted here travels under a z-key until it is. Candidates already visible from here: an ultraviolet index, and separated beta and neutron dose for stations with instruments that discriminate.

15.5.2 Energy

A station can report the electricity a site produces, consumes, trades with the grid and keeps in storage: a house with panels on the roof, a farm with a wind turbine, a cabin on a generator, a village hydro plant. One vocabulary covers all of them, because the source of the power is a word and not a key.

Key Type Meaning Quantity
produces qty power being produced, from the source in source: electrical power
consumes qty power being consumed by the whole site electrical power
grid qty power exchanged with the public grid or shore power, positive when imported electrical power
storage qty power into or out of storage, positive when discharging electrical power
charged qty how full the site's storage is proportion
load qty power drawn by one appliance or circuit electrical power
lifeproduces qty energy produced since the station's records began energy
lifeconsumes qty energy consumed since the station's records began energy
lifegridin qty energy that came in from the grid since the station's records began energy
lifegridout qty energy that went out to the grid since the station's records began energy
lifeload qty energy drawn by the appliance or circuit since the station's records began energy
t:observation f:X1HOME source:solar produces:1900W consumes:4410W grid:0W storage:2510W charged:36% ts:2026-09-14_14:36:00

122 bytes: a house in the afternoon, 1900 W from the roof, 2510 W from its battery, nothing from the grid, 4410 W in use.

Positive means flowing into the site. produces: and consumes: are never negative. grid: and storage: carry a sign, and the sign says the same thing on both: power arriving is positive, power leaving is negative. An export is grid:-1700W, and a battery being charged is storage:-3520W. One rule for both keys, rather than one per key, is what makes every packet balance:

produces + grid + storage = consumes

1900 + 0 + 2510 = 4410 in the packet above. A receiver may check the balance, and may work out the one term a site does not meter from the other three. A small difference is conversion loss in the inverter, not an error.

charged: is the site's storage, and batt: stays what section 15.5 says it is, the reporting station's own battery. They are the same number only when the station runs on the house battery it reports, and a receiver deciding whether a station survives a power cut needs the second one.

Watts and watt-hours, nothing else. Electrical power takes W and energy takes Wh (section 15.9), so any two readings compare by their digits, from a 5 W sensor node to a 48000 W hydro plant, with no prefix to scale. Whole watts are the expected precision: the reading changes by the second and its meaning by the minute, the argument section 15.5 makes for uptime:.

source: says where produces: comes from, with a word from the list in section 14.3, the same list that says what powers a station. grid and battery are never a source of produces:: the grid and storage have keys of their own, and naming them here would count the same power twice.

One source, one packet. Several sources, one more for each. A site with one source sends everything together, as above. A site with several sends the total with source:mixed, which is the packet that balances, and each source's share in a packet of its own:

t:observation f:X3FARM source:mixed produces:4600W consumes:2900W grid:-1700W ts:2026-09-14_14:36:00
t:observation f:X3FARM source:solar produces:3200W ts:2026-09-14_14:36:00
t:observation f:X3FARM source:wind produces:1400W ts:2026-09-14_14:36:00

100, 73 and 72 bytes: a farm exporting 1700 W, most of it from the roof. A packet naming one source among several carries no consumes:, grid: or storage:, because only the total balances. A produces: with no source: is production whose source the station does not know, and is read as the total.

This is the shape link: already has (section 15.6.1). A key appears once in a packet (section 4.10), so a reading that differs by source, like one that differs by bearer, travels once per source. A source that is not reported is not reported, never zero: source:wind produces:0W says the turbine is standing still, and silence says nothing, the rule cw: states in section 4.6.

load: is one appliance, never the house. A smart plug, a sub-meter on the heat pump's circuit, a clamp on the car charger, each reports what passes through it:

t:observation f:X1PLUG state:on load:65W lifeload:12400Wh ts:2026-09-14_14:36:00

80 bytes: a plug of section 11.7, switched on, with a fridge behind it drawing 65 W. What a plug measures is already inside some site's consumes:, which is why it is a key of its own: a receiver that added a house and its plugs together would count every plugged-in appliance twice.

Counters, not days. The five life keys only ever grow, like the meter in the cupboard:

t:observation f:X1HOME source:solar lifeproduces:4210000Wh lifeconsumes:3950000Wh lifegridin:1200000Wh lifegridout:1460000Wh ts:2026-09-14_14:36:00

147 bytes. There is no "today" on the wire. A day needs a clock and a midnight, and a station reporting epoch: (section 15.7) has neither; a counter needs neither. Any receiver gets any period, a day, a month, the hour since it last looked, by subtracting two readings, and a packet it missed loses nothing. The grid has two counters because it flows both ways: an export netted against an import hides both, and the meter and the bill keep them apart. A counter that falls means a new meter, and a receiver starts a new series rather than recording a negative day.

Consumption tells who is home. A house drawing 150 W at night and 3000 W at seven in the morning has told anyone listening when its people sleep, wake and go away. These readings may be redacted (section 6.2.1), and a station reporting them for its owner's displays sends them scope:local (section 9.11.1), or directly and sealed to those displays, rather than to the world. Production says much less, and a community that pools it on purpose, a village watching its shared hydro plant, is what a broadcast is for.

Asking. q:energy asks a station for its current energy readings, and the answer carries whichever of these keys it has, with s:energy:

t:request f:X1DISP d:X1HOME ts:2026-09-14_14:36:00 q:energy

59 bytes.

15.6 The radio itself

A station can measure the air, its battery and where it is. It can also measure the channel it is sitting on and the stations it can hear, and those two readings are what the rest of this format quietly assumes somebody has.

t:observation f:X3RLY7 ts:2026-08-08_14:26:40 link:lora busy:41% txtime:6%
t:observation f:X3RLY7 ts:2026-08-08_14:26:40 link:lora hears:X1QZ3N,X32DVA,CT1ABC-9
t:observation f:X3RLY7 ts:2026-08-08_14:26:40 link:ble hears:X1PZ4Q,X32DVA

74, 84 and 74 bytes. No new packet type, because design rule 5 settles it: one type carries every kind of observation, and how busy a bearer is is an observation about a radio exactly as temperature is one about the air.

Key Reading
link which bearer every reading in this packet is about
busy proportion of the last hour that bearer was occupied by anybody
txtime proportion of the last hour this station was transmitting
hears callsigns heard directly on that bearer, most relevant first
peers how many are reachable in total, so a truncated hears: is honest
mail messages held for others; omitted when none. A neighbour that can reach a recipient opens a session rather than everyone airing every message

The window is one hour and is not stated on the wire, because two stations reporting busy:41% have to mean the same thing for the number to be worth transmitting. A station that has been listening for less than an hour reports what it has and does not scale it up.

15.6.1 A reading without a bearer is not a reading

link: is required whenever busy:, txtime: or hears: appears, and a packet carrying any of them without it is discarded rather than guessed at.

This is not pedantry. A station in this format is not one radio on one channel: the same phone may be on LoRa, Bluetooth, WiFi Direct, a LAN and the internet at once, and those bearers have nothing in common. On LoRa occupancy is a legal duty cycle measured in seconds of owed silence; on a LAN it is close to meaningless; on Bluetooth what binds is scan windows rather than airtime. A single number averaged across them is not a quantity at all, and busy:41% from a gateway would be a statement no receiver could act on and every receiver would graph.

hears: has the same defect for a better reason. Hearing X1PZ4Q over Bluetooth means it is in the room; hearing it over LoRa means it is somewhere in ten kilometres of countryside. Recorded identically, those two facts are worse than either alone.

So a station reports once per bearer and says nothing it cannot mean:

link: The bearer
lora LoRa on an ISM band
ble Bluetooth Low Energy
wifi 2.4 or 5 GHz WiFi, including WiFi Direct and WiFi Aware
espnow ESP-NOW, the connectionless 2.4 GHz protocol of the ESP32 family
halow 802.11ah, sub-GHz WiFi
lan a wired or local network the station is attached to
internet reached through a gateway, wherever that gateway is
vhf VHF packet
uhf UHF packet
hf HF
cb Citizens' Band
pmr PMR446 and its regional equivalents
satellite any satellite path
other something not in this list, named in m:

The list is the one section 22.1 and spectrum.md already work through; this key only gives it a name on the wire.

Section 30.1 becomes computable because of this. It already says a station is bound by the strictest bearer it transmits on, and with one undifferentiated number nothing could tell which bearer that was. Per-bearer readings can. A phone bridging LoRa and the internet publishes link:lora busy:41% and, if it cares, link:internet busy:2%, two true statements where a single average would have been one false one.

15.6.2 Why busy: matters more than it looks

Section 31 asks every station to be disciplined about airtime and gives it nothing to measure. busy: is that measurement, and it changes politeness from a rule of thumb into a decision.

A duty cycle is a legal limit on one transmitter. It says nothing about whether forty other stations are already using the channel, and a station obeying its own 1 percent perfectly can still be the packet that collides with a call for help. A shared channel is a shared resource and the only station that can see the whole of it is the one listening to it.

So a station that hears busy: climbing should slow down what is discretionary, beacons, status posts, history replays, before the channel stops working for what is not. urg: (section 9.5) decides what survives that decision, and this is the reading that tells a station when to start making it.

txtime: is the honest half of the same measurement. A station reporting busy:60% while contributing txtime:45% of it has identified the problem, and it is itself.

15.6.3 hears: is what makes a mesh diagnosable

hears: is one station's answer to "who can you actually reach right now", directly, without a relay.

It pays for itself three ways. It says why a mesh is broken, a station nobody lists is a station nobody hears, which is a different fault from one that is heard but never answers. It says who to route through, since a carrier that lists the recipient is one hop from delivering. And it is the best evidence there is for choosing hold: in a t:mailbox declaration (section 9.12): the right mailbox is a station that other people list as heard.

Three limits, stated because a topology map invites over-reading:

The truncation of section 15.6.4 is per bearer, not a property of the list. The advert channel cuts hears: to what fits one advert; the same observation pushed to an archiver (section 12) carries the full list, over section 7.6 parts when a busy gateway hears more than one packet holds, about twenty-five callsigns fit a packet, two hundred fit nine. peers: stays the true total either way, so a cut list is always visibly cut.

15.6.4 peers: says how many were left out

t:observation f:X1A67X link:ble peers:12 hears:X1RD89,X32DVA,CT1ABC-9

69 bytes. peers: is how many stations the sender can reach directly; hears: is the ones that fitted. A busy street will not fit, about 29 six-character callsigns reach the 250-byte limit, and a shared bearer will offer less than that.

Without the count, a truncated list is a lie by omission: a reader cannot tell "these three are all there is" from "these three of forty". With it, a station that is one of thirty knows to ask rather than assume, and a client can say "3 of 12 shown" instead of drawing a map that is quietly wrong.

peers: counts what hears: would have listed in full, so the two always agree when nothing was dropped, and peers: is never smaller than the list.

15.6.5 mail: says there is something to collect

t:observation f:X1A67X link:ble peers:12 mail:3 hears:X1RD89,X32DVA,CT1ABC-9

76 bytes. mail: is how many messages this station is holding for other people (section 6.4) and would hand over if asked. It is omitted when there is nothing, because a field that is almost always 0 is a field not worth transmitting.

This is what makes carrying mail cheap. The alternative is what it replaces: airing every carried message as its own repeating broadcast, so that a passer-by might catch one. That spends the channel on messages most listeners have no use for, and spends it again every time the copy is refreshed. A beacon is on the air anyway. Saying "I have three" in six bytes lets a station that can actually reach one of the recipients ask for them directly, and lets everybody else ignore it.

So the sequence is: a station beacons mail:3; a neighbour that recognises a recipient, because it is one, or because its own hears: covers one, opens a session and takes custody; everybody else spends nothing. The count is a hint for deciding whether a session is worth the battery, not a promise about what the session will contain.

Hearing is also often asymmetric, a handheld hears a hilltop repeater that cannot hear it back. Two stations listing each other can reach each other; one listing the other cannot, and a client drawing a map should show the difference.

15.7 Stations without a clock

Station capability Key Example Meaning
keeps wall-clock time ts ts:2026-08-08_14:26:40 UTC
no clock, no storage age age:30 seconds between observation and transmission
no clock, persistent storage epoch epoch:7.4210 boot epoch 7, 4210 seconds into that epoch

The epoch form supports stations with no real-time clock. The station keeps a counter in non-volatile storage, increments it once per boot, and reports it with its seconds since boot. Two properties follow.

Ordering without a clock: between two packets from the same station, the higher epoch is later, and within one epoch the higher uptime is later.

Anchoring: a receiver holding a clock records the wall-clock time at which it first heard a given epoch, and can then date every packet of that epoch, including packets delivered days later.

A station that subsequently obtains the time sends one packet carrying both forms, anchoring that epoch for all receivers in range, and thereafter sends ts: only.


15.8 Units of measure

Every measurement carries its unit, immediately after the number and with no space between them.

alt:3048m    spd:48km/h    temp:14.2C    press:1013.2hPa    rain1:0.4mm

A person reads the value and the unit together, with no table to consult and nothing to remember about which key means what.

The unit is required, not optional. A bare number is not a measurement, it is a malformed value, and it is skipped like any other. This is the rule that separates XPRS from APRS: APRS units are implicit and positional, so a receiver that assumes the wrong one is never told it guessed. Here there is nothing to guess.

A sender transmits in the unit it works in. A boat reports knots, an aircraft reports feet and knots, a European car reports km/h, an American weather station reports Fahrenheit and inches of mercury:

t:observation f:X1BOA3 pos:38.6902,-9.4012 spd:6kt dir:275deg type:boat ts:2026-08-08_14:26:40
t:track f:CT1ABC-9 seq:3 pos:38.9012,-9.0021 alt:10000ft spd:250kt dir:47deg type:airplane ts:2026-08-08_14:26:40
t:observation f:X3WX01 pos:38.7223,-9.1393 temp:57.6F hum:78% press:29.92inHg wind:7.6mph type:wx ts:2026-08-08_14:26:40

94, 113 and 120 bytes.

15.9 The permitted units

Quantity Units Canonical
distance, altitude m, km, ft, mi, nmi m
speed m/s, km/h, mph, kt m/s
angle deg, degm deg
temperature C, F C
pressure hPa, inHg hPa
rainfall mm, in mm
duration s, min, h, day, week s
frequency Hz, kHz, MHz, GHz Hz
transmit power W, mW, kW, dBm W
irradiance W/m2 W/m2
voltage V V
proportion % %
signal power dBm dBm
signal ratio dB dB
dose rate nSv/h, uSv/h, mSv/h uSv/h
dose uSv, mSv, Sv uSv
activity concentration Bq/m3, pCi/L Bq/m3
power density uW/m2, mW/m2, W/m2 W/m2
electric field V/m, kV/m V/m
magnetic flux density nT, uT, mT, mG uT
electrical power W W
energy Wh Wh

deg is degrees true and degm is degrees magnetic. The difference is not cosmetic: magnetic declination exceeds 20 degrees in parts of the world and changes with the year, so a bearing whose reference is assumed is a bearing that is wrong by an amount nobody can recover. A station reports whichever its instrument gives it and says which that was.

mG and pCi/L are in the radiation families for the same reason: cheap EMF meters read milligauss and radon reports in some countries come in picocuries per litre, and the station's job is to report what its instrument showed, not to convert it. Conversion is the receiver's (1 mG = 0.1 uT; 1 pCi/L = 37 Bq/m3).

Each key accepts only the units of its own quantity. temp:48km/h is not a cold day, it is a malformed value, and a receiver skips it rather than trying to make sense of it.

A receiver converts to the canonical unit before it compares, stores or plots anything. Two stations reporting spd:6kt and spd:3.1m/s are reporting the same speed, and a receiver that sorts them by their digits has a bug. Conversion is the receiver's job precisely because the sender should not have to do it: a skipper who has to convert knots to metres per second before transmitting will eventually get it wrong, and nobody will notice.

The unit set is closed. A sender may not invent one, because a unit no receiver recognises makes the value unreadable rather than merely unfamiliar, and unlike an unknown key it cannot simply be skipped without losing the reading.

Coordinates are the one exception: pos: carries no unit, because it is always decimal degrees in WGS84 and no second option exists (section 15.1).


Part IV. Posting and status

What a station broadcasts about itself and the world: status, polls, blog posts, passages, and reporting.

16. Status

t:status is a short post about the sender, now. It is the packet a townhall is made of: everybody publishes, everybody sees, and a client renders them in a timeline newest first.

t:status f:X1QZ3N ts:2026-08-08_14:26:40 m:tied up in Sagres, the wind finally dropped

86 bytes. d: is absent, so it is published to anyone in range. A status carrying d: goes to that group's timeline instead of the global one, whether the group is an open name or a closed X5 (section 10).

Neither existing type would do. A t:blog post is a document: it has a title:, and a later post with the same title replaces it (section 29.1). Two statuses an hour apart are two moments and not a correction of each other, so a status has no title and never replaces anything. A broadcast t:message is conversational and spoken to whoever is listening now; a status is published, kept, and read later by people who were not, which is the distinction section 18 already draws between a message and a post.

Everything else a status needs already exists. pos: says where it was written, file: attaches a photograph, tag: files it, lang: names its language, cw: warns what it contains, scope: keeps it off the internet, n: splits a long one across up to nine parts (section 7.6), and sig: signs it. A status takes replies and reactions, which is most of the point of publishing one.

16.1 Mood

mood: says how the sender feels, so that a client can dress itself to match, a colour, a background, an icon beside the post.

t:status f:X1QZ3N ts:2026-08-08_14:26:40 mood:becalmed m:no wind since dawn, going nowhere

90 bytes. It is optional, and the four bytes it costs beyond the word itself buy a client everything it needs to theme a screen without parsing the text.

The value is one word from the list below and nothing else. A receiver that does not recognise a value skips it (section 4.3) and shows the post plainly, which is the correct outcome: the post is the content and the mood is decoration.

Family Word What it says
general blessed fortunate, and aware of it
general grateful thankful to somebody in particular
general happy plainly glad
general sad plainly not
general tired worn down rather than sleepy
general lonely alone and minding it
general proud pleased with something done
general worried expecting trouble
general calm settled, nothing pressing
general determined set on finishing something
sea becalmed no wind, going nowhere
sea adrift unmoored, in the head or the hull
sea anchored held somewhere safe
sea seasick exactly that
sea salty weathered and cheerful about it
sea stormbound kept in shelter by weather
sea landsick ashore and missing the sea
sea soaked wet through
sea homebound on the way back
sea windblown battered by a long day on deck
mountain summited on top, and it was worth it
mountain breathless thin air, not fear
mountain snowbound cannot move for snow
mountain frostbitten cold has done damage
mountain footsore too many miles today
mountain exposed on a face, nothing between you and the weather
mountain sheltered out of it at last
mountain benighted caught out by darkness
mountain acclimatised the altitude has stopped mattering
mountain whiteout cannot see, cannot navigate

Thirty words in three families. The families are there so a client can theme by family and refine later, rather than needing thirty palettes on the first day.

t:status f:X1QZ3N ts:2026-08-08_14:26:40 mood:summited pos:42.6390,0.6560 m:top of Aneto, clear all the way to France
t:status f:X1QZ3N d:X5A3F2 ts:2026-08-08_14:26:40 mood:stormbound sig:<60 characters> m:staying put another day

117 and 156 bytes.

A closed list is not a cage. Section 4.9 reserves every key beginning with z for private use, so a community that wants a mood this document does not have writes zmood:stoked beside the standard one, and every other receiver skips it without error.

mood: is defined here and, like any key, may appear on any packet the sender chooses, on a t:blog post it reads perfectly well. What it must never do is change how a packet is treated. A mood is not a priority, does not earn a relay, and does not raise a notification; urg: (section 9.5) is the field that speaks to the network, and sev: (section 24) is the one that speaks to danger.

16.2 What a status is not

Not carried. Store-and-forward (section 6.4) delivers to a recipient, and a broadcast status has none. A status that missed its audience is simply a post nobody read, which is an ordinary thing for a post.

Not privileged. Three relays, the default of section 6.1. A townhall post is not an emergency and must not compete with one.

No follow packet. A client keeps its own list of the callsigns whose statuses it shows, and that list stays on the device. Publishing it would put a permanent public record of who reads whom on the air, the same leak section 10.7 describes for rosters, and buys nothing that a local list does not already give.


17. Polls

t:poll asks everybody the same question and counts the answers.

t:poll f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 opt:sagres,lagos,portimao until:2026-08-10_18:00:00 m:where shall we meet for the net?

134 bytes, identifier 7a9b50. opt: carries the choices as comma-separated labels and m: asks the question. A poll to a group carries d: like anything else; without it, it is put to whoever is in range.

opt: takes two to six options, each a label (lowercase letters, digits and -). Two because a poll with one option is not a question, and six because a person choosing on a phone in a cockpit is not reading a menu, and because the options, the question and the envelope share 250 bytes.

17.1 until: is required

A poll states when voting closes, always. until: is the one field a poll may not omit, and a poll without it is incomplete: a counter does not count votes for it, and a client shows it as a question rather than a ballot.

This is the only field in the format that is required by its type rather than by its packet, and the reason is that the alternative is worse. A poll with no closing time never resolves. It sits in every spool that keeps it, collects votes from stations coming back into range for as long as anybody replays it, and has a different answer every time it is counted, for ever, with no moment at which anyone may say what the answer was. Section 30.3 makes that concrete: a station may keep a followed callsign's traffic indefinitely, so "eventually it ages out" is not true here.

Nothing about the requirement changes how a receiver parses a poll. Design rule 4 stands: an unknown or absent field is skipped and the packet still reads. What a missing until: costs is the count, not the parse.

The same key already carries this weight elsewhere: a carried packet must state until: (section 6.4), for the same reason, work with no deadline is work nobody can ever stop doing.

17.2 Bounding who is being asked

A poll may narrow its audience with fields the format already has, and needs no new ones:

133  t:poll f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 opt:yes,no scope:local until:2026-08-09_20:00:00 m:should we move the net to Sundays?
137  t:poll f:X3RLY7 ts:2026-08-08_14:26:40 opt:yes,no pos:37.0194,-7.9304 rad:20km until:2026-08-09_20:00:00 m:is anyone still without power?
131  t:poll f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 opt:sagres,lagos,portimao until:2026-08-10_18:00:00 lang:PT m:onde nos encontramos?
Field Narrows the poll to
d: one group, open or closed (section 10)
scope: local for the bearers in range now, or ISO country codes (section 9.11)
pos: with rad: people within that radius of that point
lang: people who read that language (section 4.7)
cw: nobody, it warns what the question contains before it renders

These say who is being asked. They do not say who may answer, and the difference is the same one section 15.7 draws about rosters. Anybody in range hears the poll, anybody may transmit a vote, and no field prevents it. What the fields do is tell a counter which votes belong in the answer and tell a client whether to put the question in front of its operator at all.

A counter should therefore be honest about what it can actually check. d: and lang: it can read off the packets. scope: it can apply to its own bearers. rad: it usually cannot check at all, because a vote carries no position unless the voter chose to include one, and requiring a position to vote asks somebody to disclose where they are in order to answer a question. A poll bounded by radius is a poll asked politely of a region, not a constituency with a roll.

17.3 Voting is a reaction

A vote needs no packet type of its own, because the format already has one that behaves exactly like a ballot:

t:reaction f:X32DVA d:LISBOA r:7a9b50 vote:sagres
t:reaction f:X32DVA d:LISBOA r:7a9b50 remove:vote

49 bytes each. Section 7.5 already says a reaction is counted once per callsign, is idempotent, is not displayed as a message and raises no notification, which is the whole specification of a vote. vote: names the chosen option and r: names the poll.

Changing your mind is voting again. The newest verifiable vote from a callsign stands, decided by ts:, exactly as a nickname is replaced (section 6.3.1). remove:vote withdraws a vote without replacing it.

A vote for an option the poll does not offer is discarded rather than counted as something else, and a vote arriving after until: is counted only if the counter chooses to, both stations may reasonably disagree about when the deadline passed, and see below.

17.4 The count is local, and provisional

There is no authoritative result and this format will not pretend otherwise. Every station counts the votes it has actually heard, and no two stations on a radio network have heard the same set. A poll that closed an hour ago is still gaining votes on the far side of a relay that has just come back up.

There is no engineering that away; it is what counting on a lossy broadcast medium means. What follows from it:

17.5 A poll is not a secret ballot

Every vote is a signed packet naming a callsign and a choice, transmitted in clear to anybody in range and relayed onward. Who voted for what is public, permanently, to everyone.

This cannot be fixed within the format. Sealing a vote with x: hides it from bystanders and not from the counter, and a vote nobody can read is a vote nobody can count. Anonymity on a broadcast medium needs cryptography this document does not have and a trusted counter this network does not want.

So: use t:poll for what time the net should start and where to meet. Do not use it to elect anybody, and do not use it for a question whose answer could cost somebody something.


18. Blog posts

t:blog publishes a piece of writing rather than sending a message. The difference is not the length. A message is addressed to someone and expects to be read once; a post is published, kept, listed and read later by people who were not listening when it went out.

t:blog f:X1QZ3N ts:2026-08-08_14:26:40 title:antenna-notes tag:radio m:The wire ends are the whole job. Everything else is decoration.

134 bytes. d: is absent, so it is published to anyone in range; a post to a group carries d: like any other packet.

18.1 Title

title: is a label: lowercase letters, digits and -, no spaces. It names the post so that it can be listed, filtered and revised, and it is unique only in combination with f:, so two stations may both publish antenna-notes.

A later post from the same station with the same title: and a newer ts: replaces the earlier one. That is how a post is corrected. A message cannot be edited and a post can, which is the second real difference between them.

title: is a slug rather than a sentence because no value except m: may contain a space. The human title is the first line of the text, where a reader expects it.

18.2 How long a post can be

A post is split across up to 9 parts like any other text (section 7.6), so its length follows from the packet limit and what the envelope costs:

Post Bytes per part Whole post
untitled, broadcast 203 1827 characters
titled 183 1647 characters
titled, one tag 173 1557 characters
titled, signed 183, less 63 on the last part 1584 characters

About 1650 characters for a normal titled post, which is three or four paragraphs. That is a short essay, not an article.

t:blog f:X1QZ3N ts:2026-08-08_14:26:40 title:antenna-notes n:1/3 m:I rebuilt the dipole this weekend and measured it properly for once.
t:blog f:X1QZ3N ts:2026-08-08_14:26:40 title:antenna-notes n:2/3 m:The feed point was three centimetres off centre, which cost about a decibel.

135 and 143 bytes, parts 1 and 2 of 3.

18.3 Files and images

file: attaches one file to a post, content-addressed like any other (section 7.7):

t:blog f:X1QZ3N ts:2026-08-08_14:26:40 title:antenna-notes file:nYxKzGm4vT2pQ8dW5jR7cL0aFbNs9hUe3oXiC6EkM1w.jpg m:The finished dipole, feed point centred at last.

162 bytes, of which 52 are the file reference and 43 of those the digest itself. The post reads on its own and the image is fetched by whoever wants it and can.

About 1500 characters is a generous budget once a title, a few tags and an image reference are paid for, and it is the same 9 parts every other kind of text gets. The limit is not raised for posts. A post split across forty packets would be unreadable until every one of them arrived, and on a channel where a single advertisement is already a lottery that is a poor way to publish. A post that genuinely needs more room is a document, and a document is a file.

18.4 Signing

A post should be signed. A message is usually one of many between two stations that know each other, but a post is read later, by strangers, after being relayed by stations the author never met, and authorship is the only thing a reader has to go on. A signature costs 65 bytes on the last part.


19. Passages

t:passage says where a vessel is going and when it expects to arrive. It is a float plan: filed before leaving so that somebody knows when to start worrying.

t:passage f:X1BOA3 pos:38.6902,-9.4012 dest:38.5241,-8.8931 since:2026-08-09_06:00:00 until:2026-08-09_18:00:00 onboard:3 type:sailboat ts:2026-08-08_14:26:40

158 bytes.

Field Meaning
pos: where it is departing from
dest: where it is bound
since: when it leaves, or left
until: when it expects to arrive
onboard: how many people are aboard
type: what kind of vessel
m: anything else worth knowing

since: and until: are the ordinary keys (section 11.1) and are not renamed for this packet: a passage is a thing with a start and an end like any other. until: is the estimated arrival, and being an estimate is what makes it useful. A vessel that has not arrived and has not cancelled by then is a vessel worth asking about.

t:passage f:X1BOA3 dest:38.5241,-8.8931 until:2026-08-09_18:00:00 onboard:3 ts:2026-08-08_14:26:40

98 bytes: bound there, back by six, three aboard. That is the whole of a float plan, and it fits in a third of a packet.

A passage is closed by sending another with the same dest: and a since: in the past, or is superseded by any later passage from the same station. Arriving and saying nothing is the case the format cannot fix, and no format can.

onboard: is a count and carries no unit, like seq: and n:. It is the number a rescue coordinator asks for first.


20. Reporting

t:report says that a packet is spam, abuse, or something worse. It is the one thing a station can do about bad traffic that is not inside a closed group, where section 10 gives a moderator hide: and everywhere else gives nobody anything.

133  t:report f:X32DVA d:X5A3F2 ts:2026-08-08_14:26:40 r:399227 kind:spam sig:<60 characters>
164  t:report f:X32DVA ts:2026-08-08_14:26:40 r:399227 kind:abuse sig:<60 characters> m:same text to every group all morning

r: names the packet being reported and kind: says why. d: aims the report at a group, whose admin and moderators are the people most able to act; without it the report is made to whoever is listening.

kind: The packet is
spam repetitive, automated, or sent to be seen rather than read
abuse directed at somebody, and meant to harm them
illegal unlawful where the reporter is
false a claim the reporter believes is untrue and dangerous
other something else, described in m:

kind: needs no new key. It already means "what kind of thing this is" for a warning, a channel and a place, and this is the fourth vocabulary it carries.

20.1 A report is a claim, never a verdict

Nothing happens automatically because a report exists. A report is not moderation: hide: (section 15.3) is the only packet in this format that hides anything, it works only inside one closed group, and only a moderator of that group may send it. A report is one station's opinion, offered to whoever finds it useful.

A report must be signed. An anonymous accusation is worth nothing to a receiver and costs the accused something, which is the worst possible ratio. An unsigned or unverifiable report is discarded.

Reports are themselves an attack. Mass false reporting is the obvious one: twenty signed reports naming an innocent station cost an attacker nothing but airtime, and a client that counts reports without weighing who sent them has built a way to silence anybody. A report counts for as much as its signer's standing with the receiver and no more, which is a local judgement, like every other judgement in this format, and cannot be delegated to the network.

Reporting is not blocking. A station that wants to stop seeing somebody mutes them locally and needs no packet and nobody's permission. A report is for telling other people, and the two should not be confused in a user interface: one is a private decision, the other is a public statement about somebody else.


Part V. Position and safety

Where a station is and what it needs: tracks, places, calls for help, warnings, notices, events, and offers and needs.

21. Tracks

A track is a named sequence of positions: a flight, a ride, a crossing. Any station may record one and publish it as it goes, and a receiver assembles the points into a line without having heard the beginning.

t:track f:X3BAL1 track:sagres-2026 seq:1 pos:38.9012,-9.0021 alt:11240m type:balloon ts:2026-08-08_14:26:40
t:track f:X3BAL1 track:sagres-2026 seq:2 pos:38.9104,-8.9772 alt:14980m climb:4.8m/s type:balloon ts:2026-08-08_14:36:00

107 and 120 bytes. track: names the track and seq: places the point within it.

t:track f:X1QZ3N seq:7 pos:38.7301,-9.1355 spd:5.2m/s dir:41deg type:bike ts:2026-08-08_14:26:40

Naming becomes worth its bytes when a station runs more than one track, or when a track is worth referring to after it ends. - When present, track: is a label: lowercase letters, digits and -, no spaces. It is chosen by the station and is unique only in combination with f:, so two stations may both run a track called commute without collision. - seq: counts from 1 and increases by one per point. A receiver that sees seq:1 then seq:4 knows two points are missing and draws the gap rather than a straight line through it. - A track point carries any observation field. Altitude, speed, course and vertical speed describe the movement; ts: dates it. - A track is never complete. There is no final packet, because a station that stops transmitting is indistinguishable from one that is out of range.

t:track f:X1QZ3N track:commute seq:7 pos:38.7301,-9.1355 spd:5.2m/s dir:41deg type:bike ts:2026-08-08_14:26:40

110 bytes.

A track packet is an observation with a name attached. It is a separate type because a receiver files it differently: an observation replaces what it knew station's position, and a track is appended to a line.

21.1 Updating a track

Later points are sent as further track packets carrying the same track: and a higher seq:. A point sent again with a seq: already held replaces it, which is how a station corrects a position it later computed more accurately.

21.2 What the station is riding on

type: names what is moving, from this set. It applies to observation and track alike.

Group Values
On foot foot, run, ski, horse
Cycles bike, ebike, motorcycle
Road car, bus, truck, tractor, emergency
Rail train, tram
Water boat, sailboat, ship, kayak
Air airplane, helicopter, glider, balloon, drone
Fixed node, digi, wx, home, portable

The words are English and are not translated on the wire. A receiver displays them in the operator's own language from this fixed set, which is the reason the set is fixed: a value invented by one station cannot be translated by another.

A receiver that does not recognise a value displays a default marker and the value as text.


22. Places

Every packet so far reports the sender: where I am, what I see, how I feel. A place reports something that is not me and does not move, a tap on a harbour wall, a mooring buoy, a bothy, the one gap in a wall of gorse. APRS has had this for thirty years as Objects and Items, and a format aimed at people at sea and in mountains cannot do without it.

t:place f:X1BOA3 ts:2026-08-08_14:26:40 kind:anchorage pos:37.0194,-7.9304 title:baleeira m:good holding in sand, exposed to south

130 bytes. kind: says what it is, pos: where it is, and title: names it.

kind: needs no new key: it already means "what kind of thing this is" for a warning and for a channel, and this is the third vocabulary it carries.

Word The place
anchorage somewhere to lie at anchor
mooring a buoy or pile to make fast to
ramp a slipway
jetty a pontoon or quay to come alongside
beach somewhere to land a small boat
fuel diesel, petrol or gas
water drinking water
repair a yard, a chandlery, somebody who mends things
shelter out of the weather, unstaffed
hut a refuge or bothy, walls and a roof
camp somewhere a tent goes
spring water out of the ground
ford a crossing
pass a way through a ridge
summit a top
trailhead where a path starts
other something not in this list, described in m:

22.1 Naming, revising and withdrawing

title: is a label and works exactly as it does on a post (section 29.1): a later place from the same station with the same title replaces the earlier one. That is how a place is corrected when the tap is moved or the buoy is lifted, and it is why a place needs no separate revision mechanism.

until: makes a place temporary, a water point that runs dry in August, a winter-only shelter. file: attaches a photograph, which for a landing beach is worth more than any description. remove:place withdraws one:

t:place f:X1BOA3 ts:2026-08-08_14:26:40 kind:water pos:37.0194,-7.9304 title:sagres-tap sig:<60 characters> m:tap by the harbour office, potable
t:place f:X1BOA3 ts:2026-09-01_09:00:00 r:9f52f6 remove:place sig:<60 characters>

189 and 126 bytes.

22.2 A place is a claim

Nothing here is a survey and no station is an authority. Two people may publish different places with the same title, or the same place in different positions, and both are true statements about what somebody believed.

The rules are the ones this format uses everywhere else. Newest wins per signer, so a station corrects itself and never anybody else. A client shows who said it, because on a coast where a mistake grounds a boat, the callsign that reported the anchorage is part of the information. An unsigned place is a claim by nobody, and a client is right to rank it below one it can verify.

A place that matters for safety is not a place. A hazard is t:warning and a call for help is t:sos; both carry a relay budget this type does not, and both are the right packet when somebody could be hurt.


23. Calls for help

t:sos is a call for help. It is a packet type rather than a flag on an observation, so that a station can act on it after reading three bytes and without understanding anything else in the packet.

t:sos f:X1QZ3N pos:38.7223,-9.1393 acc:6m kind:medical ts:2026-08-08_14:26:40 m:broken leg, cannot walk

103 bytes.

Field Required Meaning
pos: yes, if known where the person is
acc: no how well that position is known, metres
kind: no what is wrong
ts: yes when the call was made
since: no when the situation began
m: no anything a rescuer should know

kind: takes one of medical, trapped, lost, fire, water, cold, assault, vehicle, other.

Everything except ts: is optional, and a call with no position is still transmitted. A person who cannot get a fix is the person who most needs help, and a format that refuses to carry the call because a field is missing has failed at the only moment that matters.

t:sos f:X1QZ3N pos:38.7223,-9.1393 kind:trapped ts:2026-08-08_14:26:40

70 bytes: no accuracy, no message, and still actionable.

An sos is relayed up to nine times (section 6.1). It is never encrypted: a call for help that only one station can read is worth less than one anybody can. Any station may answer with s:ack, and more than one should.


24. Warnings

t:warning reports a hazard: a thing happening in a place, rather than a thing happening to the sender. A station transmits a warning about a fire it can see; it transmits an sos about a fire it is caught in.

t:warning f:X3RLY7 pos:39.4012,-8.2043 rad:5000m kind:fire sev:danger ts:2026-08-08_14:26:40 m:fast moving, wind from the north

127 bytes.

Field Meaning
pos: centre of the affected area
rad: radius of the affected area, in metres
kind: what the hazard is
sev: how bad it is
ts: when the warning was issued
m: context a person needs and the fields cannot carry

kind: takes one of fire, flood, storm, wind, snow, ice, quake, tsunami, landslide, chemical, radiation, outage, road, crowd, animal, other.

sev: takes one of the three below. A condition not serious enough for watch is a notice rather than a warning (section 25).

sev: Meaning
watch may affect you, be ready
warning will affect you, act now
danger life-threatening, leave

pos: with rad: states an area rather than a point, which is what a hazard occupies. A receiver knows whether it is inside the circle without asking anyone.

t:warning f:X3RLY7 pos:38.6902,-9.4012 rad:1200m kind:flood sev:watch ts:2026-08-08_14:26:40

92 bytes: a flood watch 1200 m around a point, with no message, because the fields already say it.

A warning is relayed up to nine times and is never encrypted, for the same reason an sos is not. ts: matters more here than anywhere else in this document: a fire warning that arrives by carrier three days later, and is plotted as current, is worse than no warning.


25. Notices

t:info reports something worth knowing that is not yet a hazard: a queue, a stopped vehicle, standing water, fog on a bend. It is the same shape as a warning and carries the same fields, and it is a separate type for two reasons.

A station filters on it after five bytes, without parsing a severity out of the middle of the packet. A subscriber who wants hazards and not road conditions gets that by type rather than by reading every packet and deciding.

And it does not inherit the relay budget of an emergency. sos and warning travel nine relays because they are worth spending a shared channel on. A traffic queue travels three, like ordinary traffic, because it is not.

t:info f:X1CAR7 pos:38.7231,-9.1402 rad:800m kind:traffic ts:2026-08-08_14:26:40 until:2026-08-08_15:30:00

106 bytes: a queue 800 m around a point, expected to clear by half past.

Field Meaning
pos: where it is
rad: how far it extends, optional
kind: what it is
ts: when it was reported
since: when it started, or will start, optional
until: when it is expected to end, optional
m: context the fields cannot carry

kind: takes one of traffic, stopped, slow, works, closure, rain, snow, ice, fog, wind, debris, animal, crowd, event, other.

There is no sev:. The type is the severity: an info is by definition not urgent, and a warning grades itself from watch to danger. A notice that turns out to matter is re-sent as a warning, which is a different packet the same one edited.

t:info f:X1CAR7 pos:38.7231,-9.1402 kind:stopped ts:2026-08-08_14:26:40 m:car on the hard shoulder, hazards on
t:info f:X3WX01 pos:38.7223,-9.1393 rad:5km kind:rain ts:2026-08-08_14:26:40 m:standing water in the underpass

110 and 110 bytes. rad: is optional: a stopped car is at a point, and standing water covers a stretch of road.

t:info f:X1QZ3N pos:38.7301,-9.1355 kind:fog ts:2026-08-08_14:26:40

67 bytes, which is the whole of it: fog, here, now.

25.1 When the condition starts and ends

Three times may appear on one packet and they answer three different questions.

Key Question
ts: when was this packet written
since: when did the condition start, or when will it start
until: when is it expected to end

ts: is a property of the packet. since: and until: are properties of the thing the packet describes, and both are optional time values.

A condition rarely begins when someone gets around to reporting it. A fire has been burning for hours before the first warning goes out, and reporting it at ts: alone makes every receiver believe it started at that moment:

t:warning f:X3RLY7 pos:39.4012,-8.2043 rad:5km kind:fire sev:danger ts:2026-08-08_14:26:40 since:2026-08-07_23:10:00

116 bytes: reported at 14:26, burning since 23:10 the previous night.

since: in the future describes something that has not happened yet, which is how planned work is announced:

t:info f:X3RLY7 pos:38.7231,-9.1402 rad:2km kind:works ts:2026-08-08_14:26:40 since:2026-08-15_07:00:00 until:2026-08-22_18:00:00

129 bytes: roadworks, announced on the 8th, starting on the 15th and expected to finish on the 22nd. A receiver does not show a condition as current before its since:. It is a plan until then, and a station that plots it as a live hazard a week early is worse than one that never received it.

since: applies to any packet describing something with a duration, including a call for help:

t:sos f:X1QZ3N pos:38.7223,-9.1393 kind:trapped ts:2026-08-08_14:26:40 since:2026-08-08_11:40:00

96 bytes: trapped since 11:40, reported at 14:26. The difference between those two is the first thing a rescuer wants to know.

A transient condition with no end is worse than no condition at all. A queue reported at eight and still on the map at midnight teaches everyone to ignore the map, and by then the packet has usually outlived the person who could withdraw it. When until: is absent a receiver applies its own expiry, and this document does not fix that interval: a fog bank and a road closure do not expire on the same clock.

25.2 Withdrawing a notice or a warning

A condition that ends before its until: is withdrawn by naming the packet that reported it:

t:warning f:X3RLY7 pos:39.4012,-8.2043 rad:5km kind:fire sev:danger ts:2026-08-08_02:10:00
t:warning f:X3RLY7 pos:39.5511,-8.1002 rad:2km kind:fire sev:watch ts:2026-08-08_09:40:00
t:warning f:X3RLY7 ts:2026-08-08_14:26:40 r:9fd8ea remove:warning

90, 89 and 65 bytes. Two fires from one station, then the first one out.

r: carries the identifier of the packet being withdrawn and remove: says what is being withdrawn. The identifier is computed, not transmitted (section 5), so both ends already have it: the first fire is 9fd8ea and the second aad744, from the sender and the second they were reported.

This is why neither a warning nor a notice needs a name of its own. Naming the kind would not do: a station that has reported two fires and withdraws fire has said nothing a receiver can act on. Naming the packet is exact, and the mechanism is the one replies, reactions and receipts already use.

remove: takes the type being withdrawn: warning, info, event, offer, need, channel, passage, blog, mailbox, service, place, vote, like or repost for a reaction. It is stated even though t: repeats it, so that a receiver can filter withdrawals of any type on one key, and so that a later revision can withdraw part of a packet rather than all of it.

A withdrawal carries no pos:, no kind: and no m:. It says one thing.


26. Events

t:event announces something happening at a time and a place: a net, a market, a meeting, a working party.

t:event f:X3RLY7 pos:38.7223,-9.1393 title:tuesday-net kind:net since:2026-08-11_20:00:00 until:2026-08-11_21:00:00 ts:2026-08-08_14:26:40 m:weekly net, all welcome

164 bytes.

title: names it, as it names a post, so an event can be revised: a later event from the same station with the same title: replaces the earlier one, which is how a time changes or a meeting is cancelled.

kind: takes one of net, meeting, market, class, work, social, race, service, other.

since: and until: are when it starts and ends. pos: is where, and rad: may give the area it covers when a point would mislead.

An event is a separate type from a notice because a receiver files it differently. A notice is a condition that is true now and expires; an event is an appointment, and belongs in a calendar rather than on a map.


27. Offers and needs

t:offer says what a station has. t:need says what it wants. They carry the same fields and differ only in direction.

t:need f:X1BOA3 pos:38.6902,-9.4012 kind:crew rad:50km ts:2026-08-08_14:26:40 m:two for a delivery to Madeira, leaving Friday
t:offer f:X1QZ3N pos:38.6902,-9.4012 kind:crew until:2026-08-20_12:00:00 ts:2026-08-08_14:26:40 m:deckhand, some night watch experience

125 and 135 bytes.

kind: takes one of crew, transport, water, food, fuel, power, shelter, berth, mooring, gear, room, tools, repair, medical, childcare, internet, storage, labour, other.

crew is in that list because it is the most common thing asked for and offered where boats gather, in both directions: t:need kind:crew is a skipper short of hands for a passage, and t:offer kind:crew is somebody willing to sail. The same pair of types covers a village after a storm and a marina in October without needing separate vocabularies.

Field Meaning
kind: what is offered or wanted
pos: where
rad: how far the sender can travel or deliver
since: when it becomes available or needed
until: when the offer or need lapses
price: what is asked or offered, if any (section 16.1)
m: the detail no vocabulary can carry
t:need f:X1QZ3N pos:38.7223,-9.1393 kind:water rad:2km ts:2026-08-08_14:26:40

77 bytes, which is all a request for water needs to be.

until: matters here as much as on a notice. An offer of a spare battery that was taken three weeks ago and never withdrawn is worse than no offer, because somebody will travel for it. A station that cannot say when its offer lapses should re-send it rather than let a receiver guess.

Neither type is relayed further than ordinary traffic (section 6.1). A need is not an emergency; a need that is an emergency is an sos.


27.1 Price

price: states what is being asked. It is optional: an offer without one has simply not named a price.

price:120EUR          once, for the thing itself
price:25EUR/day       per day
price:150EUR/week     per week
price:12.50EUR/h      per hour
price:free            nothing

An amount, a currency, and optionally / and a period. No period means a single price for the whole thing; a period means it repeats, which is the difference between selling and renting.

Periods: h, day, week, month, year.

27.2 Currencies

The currency is an ISO 4217 code: three uppercase letters, from the official list and never invented.

EUR  USD  GBP  CHF  JPY  CAD  AUD  NZD  SEK  NOK  DKK  PLN  CZK
BRL  MXN  ARS  ZAR  INR  CNY  IDR  PHP  THB  TRY  MAD  XOF  XPF

Those are examples, not the list. Any code in ISO 4217 is valid, and nothing outside it is: a receiver that meets price:120XYZ skips the field rather than displaying a number in a currency it cannot name.

No symbols. EUR and not the euro sign, because the format is ASCII, and USD rather than a dollar sign, because a dollar sign is the currency of about twenty different countries and says which one only by context a packet does not carry.

The amount follows the ordinary number rules (section 4.4): a decimal point and never a comma, so price:12.50EUR, and no thousands separator, so price:12000EUR.

27.3 When the price is not fixed

Not every price is a figure, and a seller who has not decided is common enough to deserve saying rather than leaving the field out.

price: Meaning
120EUR firm, this is the price
~120EUR negotiable, about that
~25EUR/day negotiable, and per day
offers no figure, make one
swap wants a trade, not money
free nothing

A leading ~ means the figure is a starting point rather than a demand:

t:offer f:X1QZ3N pos:38.6902,-9.4012 kind:gear price:~120EUR ts:2026-08-08_14:26:40 m:Aries windvane, needs a new bearing

121 bytes, one more than the firm version. It reads as "about 120 euros" to a person and parses as an amount with a negotiable flag to everything else, which is what a sorted list of prices needs.

offers says the seller wants to hear a number first:

t:offer f:X1QZ3N pos:38.6902,-9.4012 kind:gear price:offers until:2026-08-20_12:00:00 ts:2026-08-08_14:26:40 m:folding bike, working order

138 bytes. There is nothing to sort by, and a receiver showing it in a price column shows the word rather than inventing a zero.

swap says money is not what is wanted:

t:offer f:X1BOA3 pos:38.6902,-9.4012 kind:gear price:swap ts:2026-08-08_14:26:40 m:spare anchor for a good dinghy pump

118 bytes.

Leaving price: out entirely still means what it always meant: the sender has not said. That is different from offers, which is an invitation, and from free, which is a price.

price: works on a need as well, where it is what the sender will pay:

t:need f:X1BOA3 pos:38.6902,-9.4012 kind:berth price:30EUR/day since:2026-08-15_00:00:00 until:2026-08-22_00:00:00 ts:2026-08-08_14:26:40

137 bytes: wanted, a berth for a week in the middle of the month, at up to 30 a day.

t:offer f:X1QZ3N pos:38.7223,-9.1393 kind:crew price:free ts:2026-08-08_14:26:40

80 bytes. price:free says so explicitly, which is worth doing when the alternative is a receiver wondering whether the price was left out by accident.

27.4 The format carries no money

price: says what something costs and that is the end of this format's involvement. There are no payment packets, no balances, no receipts for money and no tipping, and the omission is deliberate rather than pending.

A packet saying a payment happened is worth exactly what the recipient's willingness to believe it is worth, and it is the highest-value forgery in any format that has one. Settlement needs a ledger somebody agrees on, and a lossy broadcast medium where every station hears a different subset of the traffic is close to the worst place to keep one.

Where value does move in this project it moves in the coin layer, which is a different system with different guarantees. A t:offer says the berth costs twenty-five euros a day; how the twenty-five euros travel is not a radio question.


Part VI. Identity and operating rules

Proving a callsign, spending airtime well, extending the format, and living alongside APRS.

28. Reserved words

q: and s: words assigned by this document: ack, read, sign, pos, batt, identity, pong, have, state, no, owner, policy, mail, energy. Command words assigned: history, file, put, set, interpret, update. Reactions assigned for add: and remove:: like, repost. All other words are reserved. A word beginning with z is private, as a key beginning with z is.


29. Proving a callsign

Anyone can write f:CT1ABC-9 on a packet. Three things already limit what that buys an impostor, and each stops short of the same place.

A signature (section 7.1) proves the packet was written by the holder of a key. It is optional, most traffic will not carry one, and a signed packet can be replayed later by anyone who heard it.

An X1, X2 or X3 callsign is derived from its own public key (section 3), so a station cannot announce one it does not hold: the characters would not match, at whatever length it announces. This does not extend to a callsign issued by a radio authority. CT1ABC-9 has no arithmetic relationship to any key, so nothing in the format prevents a second station from claiming it.

None of them proves the holder is present now. t:challenge does.

29.1 Publishing a key

A station transmits t:identity (section 7.3) periodically, not only once, because a receiver that has never heard the announcement cannot check a signature or issue a challenge. Every 30 minutes is a reasonable interval on a quiet channel; a station that changes its key announces immediately and does not wait.

q:identity (section 8) asks for one directly rather than waiting for the next period.

29.2 The exchange

The challenger generates a nonce of at least 16 random bytes, seals it to the public key the claimed callsign has announced, and sends it:

t:challenge f:X32DVA d:CT1ABC-9 ts:2026-08-08_14:26:40 k:npub1x32dva7fu9j9uenmyva7ha6x9eqwymytv2847ccv4vxdmn45y50q7hq2mv x:<64 characters>

187 bytes. k: carries the challenger's own key so the answer can be sealed back to it without a prior exchange. Where the responder already holds that key, it is omitted:

t:challenge f:X32DVA d:CT1ABC-9 ts:2026-08-08_14:26:40 x:<64 characters>

121 bytes.

Only the holder of the private key can recover the nonce. The answer is sealed to the challenger's key and names the challenge in r::

t:response f:CT1ABC-9 d:X32DVA ts:2026-08-08_14:26:40 r:35a544 x:<64 characters>

129 bytes. A challenger that gets back the value it expects has learned that the station it is talking to holds the private key for that callsign, right now.

The whole exchange fits in single packets, with room to spare on each. That is worth stating because it is the property the design has to have: a challenge that had to be split across parts could not be answered by a station that heard only some of them, and the stations most worth challenging are the ones at the edge of range.

The sizes above are not estimates. Sealing uses AES-256-CBC under a shared secret from static-static ECDH, which is what makes k: sufficient and an ephemeral key unnecessary:

Bytes
challenge plaintext, xprs-chal and a 16-byte nonce 26
padded to the block size 32
with the initialisation vector 48
base64url, no padding 64 characters

The answer seals 16 bytes and lands on the same 64 characters, the padding absorbing the difference. A public key is 63 characters. Every figure above follows from those three.

29.3 What the answer contains

The answer is never the decrypted nonce. It is

sha256(nonce | challenger callsign | responder callsign | challenge identifier)

truncated to 16 bytes and sealed to the challenger.

This matters more than it looks. A station that decrypts whatever arrives and returns the plaintext is a decryption oracle: an attacker who has intercepted a private message can submit that ciphertext as a challenge and have the victim decrypt it. Returning a hash instead means an attacker learns nothing it could not have computed by already knowing the answer.

For the same reason a station only answers a challenge whose recovered plaintext begins with xprs-chal. Ciphertext that does not decrypt to that marker is not a challenge, whatever packet it arrived in, and is discarded without a reply.

Binding the callsigns and the challenge identifier into the hash stops an answer being relayed as the answer to a different challenge, or to the same challenge put by somebody else.

29.4 Rules

29.5 What a failed challenge means

No answer is not proof of forgery. A station may be out of range, asleep, rate-limiting, running on a radio that cannot encrypt, or simply not implementing this section. Treating silence as guilt would make the network hostile to the small stations it exists for.

A wrong answer is different, and is the one case that carries weight: something claiming that callsign does not hold its key.

Outcome What it establishes
correct answer the station holds the key, and held it a moment ago
wrong answer it does not hold the key
no answer nothing

A receiver may show that a callsign has been proved recently. It should not show that one has failed, unless it failed by answering wrongly.


30. Airtime

Every other section says what a station may transmit. This one says how often, and what it owes the strangers who ask it for things. Sections 11.2 and 9.12 make that urgent: cmd:history and cmd:file let one station ask another to spend real airtime on demand, and a format that hands out that power without a budget has designed a way to flatten a solar node from across a bay.

30.1 Cadence belongs to the bearer

There is no single right interval, because the constraint is not the same on each bearer:

Bearer What binds
LoRa on ISM a legal duty cycle, often 1 percent, at SF9 a single packet owes several seconds of silence
VHF and UHF packet a shared channel and whoever else is on it
Bluetooth and WiFi Direct range, so traffic is naturally local and cheap
the internet nothing, which is the trap

A station transmits unsolicited traffic no more often than the strictest bearer it is transmitting on allows. A phone that gateways to both LoRa and the internet is bound by LoRa, not by the internet, for anything it sends to both.

Two consequences worth stating, because both have been got wrong in practice: a beacon is not free, so position, identity and service announcements go out on a period measured in tens of minutes rather than seconds; and a retry is not a new packet, so re-airing something that went unanswered counts against the same budget as saying it the first time.

Section 9.7.2 says when to stop re-airing altogether: a retry is spent only against evidence that the peer can still be reached, because a station that cannot hear its peer learns nothing by transmitting at it again, and a few stations each nursing undelivered messages for peers that left can hold a frequency down between them while delivering nothing at all.

30.2 What a station owes a stranger

cmd:history and cmd:file are requests to spend somebody else's battery. The answer is not that they must be refused, and not that they must be honoured.

t:result f:X3RLY7 d:X1BOA3 ts:2026-08-08_14:26:40 r:747ae8 code:429 sig:<60 characters> m:try X32DVA or CT1ABC-9

157 bytes. Silence and refusal look identical to the asker and mean opposite things, so a refusal that says nothing wastes the very airtime it was trying to save: the asker retries, reasonably, believing the packet was lost.

30.3 Retention belongs to the station

This document states no retention period, and will not. There is no minimum depth, no maximum, no required eviction order, and no obligation to keep anything at all. A hosting station and the software it runs decide what to keep, for how long, and for whom, and change that decision whenever storage, battery, bandwidth or interest changes.

The omission is deliberate. The stations on this network are a dongle with a microSD card, a phone that is someone's only computer, and a home server with a spare terabyte. Any number this document picked would be an overstatement for the first and an insult to the third, and it would be wrong again the day somebody adds a disk.

A spool is not a time window, and this is why no station can usefully publish one number for it. Keeping is a judgement about worth, not about age. A station holds the notes of the people its operator follows and never drops them; it keeps whatever recorded something that mattered, a rescue, a storm, a passage that went wrong, long after everything around it is gone; and it discards a stranger's chatter within hours of hearing it. Ask such a station "how far back do you go" and there is no honest answer: it goes back a year for one callsign and an afternoon for the next.

So a station advertises serve:archive and nothing more. The claim is "ask me", never "I hold everything since a date", and a station that keeps four hours of strangers should not dress that up as four months.

What it owes beyond that is plainness in the answer: code:404 for a window it does not hold, without apology or explanation. Nothing was promised, so nothing has failed.

The consequence for the asking side is the one that matters. A client must never assume any depth exists. It asks, takes what arrives, and asks somebody else for the rest. Because a replay is the original packets and duplicates collapse on their identifiers (section 11.2.1), asking three stations with overlapping spools is not waste, it is how a network with no guaranteed retention still reassembles a week nobody was awake for.

Durability here is social rather than technical: several stations keeping overlapping spools by their own choice, not one station promising to remember.

30.4 Who this protects

The stations worth protecting are the ones that cannot argue back: a solar relay on a headland, a dongle in a hut, a phone at four percent in a tent. They are also the stations that make the network reach anywhere interesting.

A budget is therefore not a limitation on generosity but the thing that makes generosity survivable. A relay that serves until its battery dies has served nobody by morning.


31. Adding a field, worked

A format is judged by what it costs to add something it did not foresee. Suppose a station gains an air-quality sensor.

The implementer takes an unused key, gives it a type and a unit, and transmits it:

t:observation f:X3WX01 pos:38.7223,-9.1393 temp:14.2C zpm:8 ts:2026-08-08_14:26:40

82 bytes. The new field costs six bytes. Every existing receiver reads zpm:8, does not recognise the key, skips it, and continues at ts:. Nothing is versioned, nothing is negotiated, and no other field is affected.

The key begins with z because unassigned keys belong in the private space. If this document later assigns it, the entry is added to the table in section 15.3 with its type and unit, and a shorter key may be chosen; nothing else changes.

The same holds for a new word in q: and s:. A station asking q:pos,co2 gets s:pos from every station built before CO2 existed, with no error and no negotiation.


32. Operating alongside APRS

A licensed amateur may bridge XPRS and APRS under their own callsign and responsibility, subject to section 7.4. An X1, X2 or X3 callsign is generated by the station itself and assigned by no authority, so traffic from such a callsign must not be originated onto amateur infrastructure. Ciphertext must never be placed on APRS, both because APRS is a 7-bit protocol that would corrupt it and because obscured meaning is not permitted on amateur bands.


Part VII. Reference

Worked examples, the cheat sheet, reserved words, and what is actually implemented.

33. Examples

33.1 Position

Coarse position, station with no clock:

t:observation f:X1QZ3N pos:38.72,-9.14 age:30

45 bytes.

Normal fix with a clock:

t:observation f:X1QZ3N pos:38.7223,-9.1393 ts:2026-08-08_14:26:40

65 bytes.

Five decimals of arithmetic, eight metres of measured accuracy:

t:observation f:X1QZ3N pos:38.72231,-9.13934 acc:8m ts:2026-08-08_14:26:40

74 bytes.

33.2 Movement

Person on foot:

t:observation f:X1QZ3N pos:38.7223,-9.1393 alt:87m type:foot spd:1.4m/s dir:212deg ts:2026-08-08_14:26:40

105 bytes.

Vehicle, with a note:

t:observation f:X1CAR7 pos:38.7231,-9.1402 alt:87m spd:13.4m/s dir:212deg acc:8m type:car ts:2026-08-08_14:26:40 m:heading south on the N8

138 bytes.

Balloon ascending at 4.8 m/s through 11240 m:

t:observation f:X3BAL1 pos:38.9012,-9.0021 alt:11240m climb:4.8m/s spd:9.2m/s dir:47deg type:balloon ts:2026-08-08_14:26:40

123 bytes.

Vessel under way, no altitude:

t:observation f:X1BOA3 pos:38.6902,-9.4012 spd:3.1m/s dir:275deg type:boat ts:2026-08-08_14:26:40

97 bytes.

33.3 Weather

Station with three sensors:

t:observation f:X3WX01 pos:38.7223,-9.1393 temp:14.2C hum:78% press:1013.2hPa type:wx ts:2026-08-08_14:26:40

108 bytes.

Every defined weather field plus battery, fourteen fields:

t:observation f:X3WX01 pos:38.7223,-9.1393 temp:14.2C hum:78% press:1013.2hPa wind:3.4m/s wdir:210deg gust:7.1m/s rain1:0.4mm rain24:12.6mm solar:640W/m2 batt:96% type:wx ts:2026-08-08_14:26:40

193 bytes, leaving 65 for fields not yet defined. It is the longest packet in this document.

Indoor sensor with no position and no clock, using the indoor keys so the reading is not taken for outside air. Position is omitted rather than sent as zero:

t:observation f:X3WX01 intemp:21.5C inhum:54% age:60

52 bytes.

33.4 Telemetry

Unattended node reporting power state:

t:observation f:X3RLY7 pos:38.7810,-9.2043 alt:210m batt:64% volt:12.9V type:node ts:2026-08-08_14:26:40

104 bytes.

33.5 Emergency

A call for help is its own packet type, not an observation with a flag on it (section 23):

t:sos f:X1QZ3N pos:38.7223,-9.1393 acc:6m kind:medical ts:2026-08-08_14:26:40 m:broken leg, cannot walk

103 bytes, identifier bfa3f1. Any station may answer:

t:receipt f:X32DVA d:X1QZ3N r:bfa3f1 s:ack

42 bytes.

33.6 Reachability

t:ping f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:26:40
t:pong f:X3RLY7 d:X1QZ3N ts:2026-08-08_14:26:40 rssi:-92dBm snr:7.5dB

47 and 69 bytes. The reply reports the signal the test arrived with, which is the receiver's measurement, not the sender's.

33.7 Reading a packet

t:observation f:X3RLY7 pos:38.7810,-9.2043 alt:210m temp:11.8C hum:88% press:1008.4hPa type:node ts:2026-08-08_14:26:40

119 bytes.

Field Type Reading
t:observation enum an observation; a station filtering for messages stops here
f:X3RLY7 call unattended station
pos:38.7810,-9.2043 coord 38.7810 N, 9.2043 W, four decimals, so about 11 m
alt:210 dec 210 m above mean sea level
temp:11.8 dec 11.8 degrees Celsius
hum:88 int 88 percent relative humidity
press:1008.4 dec 1008.4 hPa at station level
type:node enum unattended node
ts:2026-08-08_14:26:40 time UTC

There is no d:, so it is addressed to no one in particular. There is no q:, so nothing is expected back.


34. Worked exchanges

34.1 Group conversation with a reply and a reaction

1  t:message f:X1QZ3N d:LISBOA ts:2026-08-08_14:26:40 m:net starts in ten minutes
2  t:message f:X1RD89 d:LISBOA ts:2026-08-08_14:36:00 r:399227 m:I'll be late, start without me
3  t:reaction f:X32DVA d:LISBOA r:399227 add:like

78, 92 and 46 bytes. Packet 1 transmits no identifier; every receiver computes 399227 from its sender, time and text. Packets 2 and 3 name that value. Packet 2 has its own computed identifier and can be replied to in turn.

34.2 Direct message with both receipts

1  t:message f:X1QZ3N d:X1RD89 ts:2026-08-08_14:26:40 q:ack,read m:did you get the keys?
2  t:receipt f:X1RD89 d:X1QZ3N r:9821a4 s:ack
3  t:receipt f:X1RD89 d:X1QZ3N r:9821a4 s:read

85, 42 and 43 bytes. Packet 1 asks for two things by name and packets 2 and 3 answer with the same names.

34.3 Request and partial answer

1  t:request f:X1QZ3N d:X3RLY7 ts:2026-08-08_14:26:40 q:pos,batt
2  t:observation f:X3RLY7 pos:38.7810,-9.2043 ts:2026-08-08_14:26:40 s:pos

61 and 71 bytes. The station has no battery reading. It answers with what it has, as a broadcast, and says in s: which request that satisfied, so the asker is not left waiting.

34.4 A long group message

1  t:message f:X3RLY7 d:LISBOA ts:2026-08-08_14:26:40 n:1/3 m:The repeater on the hill is down.
2  t:message f:X3RLY7 d:LISBOA ts:2026-08-08_14:26:40 n:2/3 m:We swapped the antenna feed this morning
3  t:message f:X3RLY7 d:LISBOA ts:2026-08-08_14:26:40 n:3/3 m:and it is back up, but only just.

92, 99 and 92 bytes. Reassembly is keyed on (X3RLY7, 2026-08-08_14:26:40). Joined with one space between parts:

The repeater on the hill is down. We swapped the antenna feed this morning and it is back up, but only just.

If part 2 never arrives, the set is discarded after 10 minutes and nothing is displayed.

34.5 Clockless weather station anchored by a neighbour

1  t:observation f:X3WX01 pos:38.7223,-9.1393 temp:14.1C hum:80% epoch:7.3600
2  t:observation f:X3WX01 pos:38.7223,-9.1393 temp:14.2C hum:78% epoch:7.4210
   A receiver holding a clock records: epoch 7 heard at 2026-08-08_14:26:40.

3  t:observation f:X3WX01 pos:38.7223,-9.1393 epoch:7.9930 ts:2026-08-08_14:26:40
   The station has obtained the time and anchors epoch 7 for all receivers.

4  t:observation f:X3WX01 pos:38.7223,-9.1393 temp:15.0C hum:74% ts:2026-08-08_14:36:00

54, 54, 65 and 67 bytes. Packets 1 and 2 are orderable without any clock, since the higher uptime is later. Packet 3 makes the anchor explicit.


35. Cheat sheet

Everything the format defines, on one page. Each entry is stated in full in the section it belongs to; nothing here is new.

key:value key:value key:value ...

Fields separated by one space. t: first, m: last, and only m: may contain spaces. Keys are 1 to 6 characters, lowercase letters and digits, beginning with a letter. An unknown key or an unknown type is skipped, never an error. Maximum packet 250 bytes, on every transport.

Packet types

t: Purpose
message a message, to a station, a group, or anyone in range
observation an observation: position, movement, weather, telemetry
receipt a receipt or an answer to a request
reaction a reaction to another message
request a request for data another station holds
identity an identity announcement, binding callsign to public key
track a point in a named track (section 21)
sos a call for help (section 23)
info a notice about conditions (section 25)
blog a published post (section 18)
poll a question put to everybody, with the choices (section 17)
file what a file is, so it can be wanted (section 7.7.1)
report a claim that a packet is spam or abuse (section 20)
place somewhere useful that is not the sender (section 22)
status a short post about the sender, now (section 16)
passage where a vessel is going (section 19)
event something happening at a time and place (section 26)
offer what a station has (section 27)
need what a station wants (section 27)
channel a frequency a station uses (section 14)
mailbox stations that hold mail for the sender (section 9.12)
service what a station does for others (section 13)
command asks a station to do something (section 11)
result what happened to a command
moderate an act of authority in a group (section 10)
challenge a challenge to prove a callsign (section 29)
response the answer to a challenge
warning a warning about a hazard (section 24)
ping a reachability test
pong a reply to ping

Envelope keys

Key Type Meaning
t enum packet type, always the first field
f call from: the sending callsign
d call destination: a callsign, a group name, or absent for a broadcast
ts time when the packet was composed, UTC
tz offset the sender's offset from UTC, for display
q words what the sender wants back (section 8)
s words what this packet answers or reports (section 8)
r hex6 the identifier of another packet this one refers to
n ratio this packet is part i of n
tag labels topic labels chosen by the sender (section 4.5)
cw words what the packet contains, warned before rendering (section 4.6)
urg enum how much this is worth carrying (section 9.5)
scope scope how far this may be relayed, default global (section 9.11)
lang lang language of m:, default English (section 4.7)
hold path preferred mailboxes, in order (section 9.12)
serve words what a station does for others (section 13)
cmd label the action a command asks for (section 11)
owner path who may command a station: its allow-list (section 11.9)
use enum who may originate traffic through a station (section 11.9)
first path senders whose packets a station airs ahead of others (section 11.9)
ssid text the WiFi network a station is to join, sealed (section 11.10)
pass text its password, only ever sealed (section 11.10)
wifi enum a station's WiFi: join, off; on a result up, joining, failed, off (section 11.10)
ip text the IPv4 address a station holds on its network, dotted, on a result (section 11.10)
zone enum a station's time zone: auto, or an offset (section 11.10)
ap enum a station's own access point, on or off (section 11.10)
key enum new: a station is to make a new key (section 11.10)
nsec bech32 a private key a station is to take, only ever sealed (section 11.10)
arg words its arguments
code int what happened, on a result
near qty how close to dest counts as arrived (section 6.4)
relay path callsigns the sender asks to relay this packet, in order (section 9.2.2)
route path the route a receipt is acknowledging (section 9.10)
add enum something this packet adds (section 7.5)
remove enum something this packet withdraws (section 7.5)
grant path callsigns admitted to a group (section 10)
revoke path callsigns removed or suspended (section 10)
role enum what a grant confers: mod, sub, or absent for a member
hide enum what a moderator withdraws from view: message
mood enum how the sender feels (section 16.1)
only call narrows a replay to one callsign or group (section 11.2)
opt labels the choices in a poll, two to six (section 17)
root hex6 the packet a thread hangs from (section 7.4)
size qty how large a file is (section 7.7.1)
vote label the option chosen in a poll (section 17.3)
via path callsigns that relayed this packet, oldest first (section 9)
track label name of a track this packet belongs to (section 21)
title label name of a post or event, stable across revisions
dest coord where a passage is bound (section 19)
onboard int how many people are aboard
price money what is being asked or offered (section 16.1)
freq qty a frequency (section 14)
bw qty bandwidth
shift qty repeater input, as an offset from freq
input qty repeater input frequency, stated outright
tone qty access tone
power qty transmit power
mode enum how a channel is modulated
ch label channel number in a band plan
range qty expected usable range, an estimate
site enum whether the station stays where it is
source enum what powers the station, or where produces: comes from (sections 14.3, 15.5.2)
every qty how long between recurring windows
for qty how long each window lasts
at clock time of day a cycle is anchored to, UTC
seq int position of this point within that track
kind enum nature of an event, values per packet type (sections 15, 16); in cmd:history the packet type asked for, or a comma-separated list of them (section 11.2)
sev enum severity of a warning (section 24)
rad qty radius of the area affected or asked about (sections 16, 17, 28)
since time when the condition started, or will start
until time when the sender expects the condition to end
m text human-readable content, always last
file ref content hash and type of a referenced file
name label filename, when the extension is not enough (section 7.7.1)
ph ref content hash of a file's piece list (section 7.7.2)
count int on t:file kind:folder, how many files a listing holds (7.7.3); on an archiver's serve:archive announcement, how many RECORDS it holds, never how many callsigns (13.0.1)
b b64 a small file's bytes, inline (section 7.7.4)
ih label BitTorrent infohash, 40 hexadecimal characters (section 7.7.5)
have b64 what a station holds of a file: a chunk or piece bitfield, least significant bit first; or, the two stated exceptions, the word full and a fraction such as 412/900 (section 8.1)
off qty byte offset a cmd:file transfer resumes from (section 11.2)
x b64 sealed body
xr b64 hidden parts of a redacted packet (section 6.2.1)
sig base85 signature
k bech32 public key, in t:identity and t:challenge, and on q:owner, a claim and a new key's answer (sections 11.9, 11.10)

Position and movement

Key Type Meaning Quantity
pos coord position degrees
alt qty altitude above mean sea level distance
acc qty horizontal accuracy radius distance
spd qty speed over ground speed
dir qty course over ground, the direction it is travelling angle
o qty heading, the direction it is pointing angle
climb qty vertical speed, signed speed

Weather

Key Type Meaning Quantity
temp qty air temperature, outdoors temperature
hum qty relative humidity, outdoors proportion
intemp qty air temperature, indoors temperature
inhum qty relative humidity, indoors proportion
press qty barometric pressure, station level pressure
wind qty wind speed, sustained speed
wdir qty wind direction, the direction it blows from angle
gust qty wind gust, peak speed
rain1 qty rainfall, previous hour rainfall
rain24 qty rainfall, previous 24 hours rainfall
solar qty solar irradiance irradiance

At sea

Key Type Meaning Quantity
wave qty significant wave height distance
swell qty swell period duration
seatemp qty sea surface temperature temperature
vis qty horizontal visibility distance

Telemetry and station type

Key Type Meaning Quantity
batt qty battery charge proportion
volt qty supply voltage voltage
link enum which bearer a reading is about (section 15.6)
busy qty proportion of the last hour that bearer was occupied (section 15.6) proportion
txtime qty proportion of the last hour this station transmitted proportion
hears path callsigns heard directly, most relevant first (section 15.6.3)
peers int how many stations are reachable in total (section 15.6.4)
mail int messages held for other stations (section 15.6.5)
rssi qty received signal strength signal power
snr qty signal-to-noise ratio signal ratio
uptime qty how long the station has run without interruption duration
lifetime qty how long the station has run in total, across every restart duration
odometer qty distance travelled over the station's service life distance
type enum what the station is or is riding on, from the set in section 21.2
state enum a device's principal condition, from the closed list in section 11.7
level qty how far, when a condition is partial (section 11.7) proportion
target qty the setpoint a device holds a reading at (section 11.7)

Radiation (section 15.5.1)

Key Type Meaning Quantity
dose qty ambient ionizing dose rate dose rate
lifedose qty ionizing dose accumulated since records began dose
radon qty radon activity concentration in the air activity concentration
rf qty radio-frequency power density power density
efield qty electric field strength electric field
mfield qty magnetic flux density magnetic flux density

Energy (section 15.5.2)

Positive flows into the site: produces + grid + storage = consumes. Watts and watt-hours only. source: names where produces: comes from; several sources send a source:mixed total plus one packet per source.

Key Type Meaning Quantity
produces qty power produced, from source: electrical power
consumes qty power consumed by the whole site electrical power
grid qty grid exchange, positive when imported electrical power
storage qty storage flow, positive when discharging electrical power
charged qty how full the site's storage is proportion
load qty power drawn by one appliance or circuit electrical power
lifeproduces qty energy produced since records began energy
lifeconsumes qty energy consumed since records began energy
lifegridin qty energy in from the grid since records began energy
lifegridout qty energy out to the grid since records began energy
lifeload qty energy drawn by the appliance or circuit since records began energy

Time

Station capability Key Example Meaning
keeps wall-clock time ts ts:2026-08-08_14:26:40 UTC
no clock, no storage age age:30 seconds between observation and transmission
no clock, persistent storage epoch epoch:7.4210 boot epoch 7, 4210 seconds into that epoch

ts: is when the packet was written; since: and until: are when the thing it describes begins and ends. All are YYYY-MM-DD_HH:MM:SS in UTC. tz: carries the sender's offset, for display only.

Units

Every measurement carries its unit, immediately after the number, with no space.

Quantity Units Canonical
distance, altitude m, km, ft, mi, nmi m
speed m/s, km/h, mph, kt m/s
angle deg, degm deg
temperature C, F C
pressure hPa, inHg hPa
rainfall mm, in mm
duration s, min, h, day, week s
frequency Hz, kHz, MHz, GHz Hz
transmit power W, mW, kW, dBm W
irradiance W/m2 W/m2
voltage V V
proportion % %
signal power dBm dBm
signal ratio dB dB
dose rate nSv/h, uSv/h, mSv/h uSv/h
dose uSv, mSv, Sv uSv
activity concentration Bq/m3, pCi/L Bq/m3
power density uW/m2, mW/m2, W/m2 W/m2
electric field V/m, kV/m V/m
magnetic flux density nT, uT, mT, mG uT
electrical power W W
energy Wh Wh

deg is true and degm is magnetic. A receiver converts to the canonical unit before comparing, storing or plotting. pos: is the one measurement with no unit: always decimal degrees, WGS84.

Numbers

The decimal separator is ., never ,, because a comma already separates latitude from longitude and words in a list. No thousands separator: alt:11240m. Leading - for negative, no +, no exponent. A digit before the dot, never a trailing dot. Trailing zeros are significant.

Asking and answering

q: asks and s: answers with the same words, several separated by commas.

Assigned: ack, read, sign, pos, batt, identity, pong, have, state, no, owner, policy, mail, energy.

s:no means the request will not be served at all. A partial answer names only what it satisfied.

A direct message between two stations that have exchanged one before is answered with t:receipt ... s:ack without q:ack (section 9.7.1). Broadcasts, group and regional messages, receipts, and strangers are never answered automatically.

What a station is, or is riding on

type:

Group Values
On foot foot, run, ski, horse
Cycles bike, ebike, motorcycle
Road car, bus, truck, tractor, emergency
Rail train, tram
Water boat, sailboat, ship, kayak
Air airplane, helicopter, glider, balloon, drone
Fixed node, digi, wx, home, portable

What an event is

kind:, scoped to the packet type it appears in.

Packet Values
sos medical, trapped, lost, fire, water, cold, assault, vehicle, other
warning fire, flood, storm, wind, snow, ice, quake, tsunami, landslide, chemical, radiation, outage, road, crowd, animal, other
info traffic, stopped, slow, works, closure, rain, snow, ice, fog, wind, debris, animal, crowd, event, other

sev:, on a warning only:

sev: Meaning
watch may affect you, be ready
warning will affect you, act now
danger life-threatening, leave

Content warnings

cw:, one or more words separated by commas.

Word Contents
adult sexual content
nudity nudity that is not sexual
violence violence
injury graphic injury, blood, surgery
death death, human or animal
drugs drug or alcohol use
language profanity
spoiler spoils something the reader may not have seen
flashing rapid flashing or strobing
other something else the sender thinks needs a warning

Repeated on every part, covers any file:, stays in cleartext when the body is sealed, never stripped by a relay. Absence is not a guarantee.

Prices

120EUR        firm          ~120EUR       negotiable
25EUR/day     per day       ~25EUR/day    negotiable, per day
offers        make one      swap          wants a trade
free          nothing       (absent)      not stated

Currency is an ISO 4217 code, three uppercase letters, never a symbol. Periods: h, day, week, month, year.

Channels

t:channel, one packet per frequency. power: present means the station transmits there; absent means it only listens.

kind:    listen simplex repeater beacon net gateway emergency other
mode:    fm am usb lsb cw ssb packet aprs lora ft8 psk31 rtty dmr dstar c4fm m17 dv other
site:    fixed mobile portable temporary
source:  grid battery solar wind hydro marine geothermal biomass diesel gasoline
         natgas lpg hydrogen nuclear fuel generator mixed other

Recurring windows: every: between them, for: how long each lasts, at: the UTC time of day the cycle is anchored to, default 00:00:00. The 3-3-3 plan is ch:3 every:3h for:3min. since: and until: bound the schedule itself.

freq: is what you tune to hear the station. A repeater's input is shift: as an offset or input: outright, the latter for cross-band.

A t:channel WITH d: is a working-channel invitation (section 14.7): meet me there, until: how long I wait, r: the exchange it serves, link: + ch: when the place is a technology (wifi, espnow) rather than a frequency. Signed or ignored. Choreography: accept with s:ack on the commons (or s:no, reason in m:) -> both tune -> the invitee RE-AIRS its acceptance on the working channel ("I am also here") -> only then does the bulk sender transmit -> done or until:, everyone returns to the calling channel. Groups: d: a group name, accept individually, arrive individually.

range: is the operator's estimate, not a guarantee.

Carrying toward a place

dest: where it is bound, near: how close counts as arrived, until: when to stop (required, never more than a year out), urg: low normal high urgent. A carrier takes a copy only if it expects to get closer. Not bound by the three-relay limit.

lang: names the language of m:, default English: PT, or PT/BR for a regional variant.

nick: is a signed, human-readable name on t:identity. Shown only when the signature verifies, newest ts: wins, and never usable as an address.

Commands

t:command with cmd: and arg:; t:result with code: naming it in r:.

200 done   202 accepted   400 bad args   403 refused
404 unknown   408 too old   500 failed

Answer at once with 202 even when the work takes minutes; any number of results may name one command. Splits across parts like a message; cmd:interpret puts a natural-language instruction in m: for a station that reads it, and a reply names the reassembled packet rather than any part. Must be signed, expires after 300 s unless until: says otherwise, never carried, never shown as a message. Authentication is not authorisation, the allow-list is the bot's.

cmd:set makes a device state true (section 11.7): state: from the closed list on off open closed locked unlocked (motion clear pressed are report only), level: for the partial degree, target: for a setpoint. The result echoes what IS. Unsigned is discarded; signed and unknown is 403.

A station is owned (section 11.9): unowned, it asks q:owner on local bearers and the first uncarried signed cmd:set owner: claims it; the owner sets owner: (the allow-list), use: (all listed owners none, who may originate through it), first: (who is aired first) and serve:; anyone asks q:policy. Send order is fixed: sos/warning, then first:, then urg:, then ts:. A policy ts: not later than the last accepted one is 408.

t:service advertises what a station does: relay archive internet aprs nostr files devices time weather wifi other. Physical goods are t:offer, not this. A claim about capability, never evidence of good faith. devices means the station is a controller: each automated device it operates is an X4 station with its own keypair, held and signed for by the controller (section 11.7.1).

t:mailbox names the stations that hold mail for the sender, hold: in order of preference. Several coexist, each optionally bounded by since: and until:; the narrowest window containing the moment wins. Cancel one with r: and remove:mailbox. All of it must be signed, and an unverifiable one ignored.

scope: limits where a packet goes: absent or global anywhere, regional on local and regional bearers (LoRa, HaLow, radio) and never the internet or a satellite, local only on BLE, WiFi Direct, WiFi Aware, ESP-NOW and a LAN, or ISO country codes. It binds at both ends of a hop: a packet heard on a bearer wider than its scope goes no further. An unknown scope word stops relaying. Not carried when local, and binding on gateways. Reception is never restricted, only relaying. A group is an address, not a boundary, only x: keeps content private.

Catching up, and fetching

t:command f:X1BOA3 d:X3RLY7 ts:... cmd:history since:... sig:...
t:command f:X1BOA3 d:X3RLY7 ts:... cmd:history since:... until:... only:X5A3F2 sig:...
t:command f:X1QZ3N d:X3RLY7 ts:... cmd:file file:nYxKz...M1w.jpg sig:...
t:command f:X1QZ3N d:X3RLY7 ts:... cmd:file file:nYxKz...M1w.jpg off:64kB sig:...
t:command f:X1QZ3N d:X3RLY7 ts:... cmd:put file:nYxKz...M1w.jpg size:2MB until:... sig:...

Standard commands carry parameters in named keys, never arg:. The station answers code:202, re-airs the original packets unchanged, then code:200. 206 instead means that was one page and more exists: the replay runs newest first, so continue by moving until: to the oldest ts: you received and asking again. No cursor, no session. 404 nothing held, 403 refused, 429 over budget with alternatives in m:. Derived identifiers make the replay safe: a duplicate collapses on the identifier it already had, so there are no cursors and overlapping windows cost only airtime.

A continuation must make progress. Two rules, one on each side, because this is the exchange that silently loops when either is missing:

Measured, because both halves failed at once: a resume mark that walked FORWARD with the clock (each continuation asking for a slightly newer slice than the last), and then, once that was fixed, one that stopped moving at all. A caller that reads a stuck loop as a busy archiver polls it at its fastest cadence forever, which is the most expensive possible response to an archiver that is telling it nothing. Advertise a spool with serve:archive, files with serve:files.

Mentions, threads and files

A mention is @CALLSIGN inside m:, no key. @ then uppercase letters, digits, - and /, ending at the first character that is not one; uppercase is why me@example.com is not a mention. No limit but the packet limit. Being named raises a notification and is the strongest signal for what to keep. Anyone may mention anyone, so a client must offer to mute.

root: names the packet a thread hangs from. A first-level reply carries r: alone; deeper replies carry both, so a lost middle packet no longer orphans everything beneath it.

t:file says what a file is: file: the hash (43 base64url characters, a dot, the type), size: with its unit, tag: topics, m: the description, optionally name: the filename and ph: the piece list's own hash. size: is what lets a station decline before starting a fetch it cannot finish, and the description is the only thing that makes a file findable by words rather than by hash.

Files, the whole flow (section 7.7): a listing file (XFL1, one ref size name line per file, sorted by name) is a folder; announce it with t:file kind:folder count: size:; fetch anything with cmd:file (resume with off:), find holders with q:have (have:full, a bitfield, or 412/900), deposit with cmd:put file: size: [until:] and the signed code:200 is the custody receipt. A file up to 896 bytes rides inline in b: over section 7.6 parts, joined with nothing. Piece lists (sha size per line, piece order) let partial holders serve what they verified. The BitTorrent infohash is derived deterministically and never needs transmitting between stations.

Archivers (section 12.9): content only from chosen depositors, NEVER from another archiver. Between archivers only the directory travels, an XDIR1 listing, one call ts line per archived callsign, announced with t:service serve:archive count: file:<ref>.xdir and fetched like any file. A miss answers code:404 m:try <peers>. Discovery: serve:archive on the air, or another archiver's verbatim copy of the signed announcement.

The radio itself

t:observation f:X3RLY7 ts:... link:lora busy:41% txtime:6% hears:X1QZ3N,X32DVA

link: names the bearer and is required, lora ble wifi espnow halow lan internet vhf uhf hf cb pmr satellite other, because a station here is not one radio on one channel, and a figure averaged across LoRa and a LAN is not a quantity. A reading without it is discarded; report once per bearer.

busy: how much of the last hour that bearer was occupied by anybody, txtime: how much of it was this station, hears: callsigns heard directly on it, most relevant first (the sender decides what relevant means), peers: how many are reachable in total so a truncated list is honest. Window is one hour and is never on the wire, so numbers compare. Keys on t:observation, never a new type, design rule 5.

busy: is what section 30 was missing: a duty cycle limits one transmitter and says nothing about the forty others already on that bearer. Rising busy: means slow down what is discretionary, beacons, statuses, history replays, and let urg: decide what survives. A station reporting busy:60% with txtime:45% has found the problem and it is itself.

hears: says why a mesh is broken, who to route through, and who is worth naming in hold:. Hearing is often asymmetric, two stations listing each other can reach each other; one listing the other cannot.

Reporting

t:report f:X32DVA d:X5A3F2 ts:... r:399227 kind:spam sig:...

kind: is spam abuse illegal false other. Must be signed. A report is a claim, never a verdict, nothing acts on it automatically, and hide: (section 15.3) remains the only packet that hides anything. Mass false reporting is the obvious attack, so a report is worth its signer's standing and no more. Reporting is not blocking: muting is local, needs no packet and tells nobody.

Polls, and passing things on

t:poll f:X1QZ3N d:LISBOA ts:... opt:sagres,lagos,portimao until:... m:where shall we meet?
t:reaction f:X32DVA d:LISBOA r:7a9b50 vote:sagres
t:reaction f:X32DVA d:LISBOA r:7a9b50 remove:vote

opt: is two to six labels. until: is required, a poll without it is not counted, because a poll that never closes has a different answer every time anybody replays it. Narrow the audience with d: (a group), scope: (local or country codes), pos: with rad:, or lang:; those say who is being asked, never who may answer, and a counter usually cannot check rad: at all because a vote carries no position.

A vote is a reaction, so it is one per callsign, idempotent and withdrawable; voting again replaces the earlier vote. The count is local and provisional, every station counts what it heard, the author's tally is not authoritative, and a client that shows "7 votes" where it means "7 that reached me" has lied by rounding. Not a secret ballot: who voted for what is public and permanent.

Reply is r:. A quote is a reply that carries m:, no separate mechanism. A repost is add:repost / remove:repost on a reaction, and what travels is the original packet with f:, ts: and sig: untouched, so duplicates collapse on the identifier and a post reposted by nine stations is still one post. A repost adds nothing and so cannot misrepresent; a quote is your own packet with your own words.

Places

t:place reports something that is not you and does not move. kind: from:

anchorage mooring ramp jetty beach fuel water repair
shelter hut camp spring ford pass summit trailhead other

pos: where, title: names it and a later place with the same title from the same station replaces it, until: for temporary, file: for a photograph, remove:place to withdraw. Newest wins per signer; a client shows who said it. A hazard is t:warning and a call for help is t:sos, both carry a relay budget a place does not.

Airtime

Unsolicited traffic is bound by the strictest bearer a station transmits on, not the loosest. A beacon is not free; a retry is not a new packet. Serving yourself is unmetered, serving a stranger is optional, metered and bounded per period (section 29.4's precedent). Refuse out loud with code:429 and name somebody else, silence and refusal look identical to the asker and mean opposite things.

Retention is the station's own, this format sets no period, no minimum and no eviction order. A spool is not a time window: a station may keep a followed callsign for a year and a stranger for an afternoon, so it advertises serve:archive and never a depth. Answer code:404 for a window you no longer hold. A client assumes no depth: ask, take what arrives, ask somebody else for the rest.

Status

t:status is a short post about the sender, now, the townhall packet. No title: and it never replaces an earlier one, which is what separates it from t:blog. d: absent publishes to anyone in range; d: on a group makes it that group's timeline. Three relays, never carried, replies and reactions both. Following is a list on the client and never a packet.

mood: is optional and one word from this list, for theming only:

general   blessed grateful happy sad tired lonely proud worried calm determined
sea       becalmed adrift anchored seasick salty stormbound landsick soaked
          homebound windblown
mountain  summited breathless snowbound frostbitten footsore exposed sheltered
          benighted acclimatised whiteout

An unrecognised mood is skipped and the post shown plainly. A mood never earns a relay, a priority or a notification, urg: speaks to the network and sev: to danger. For a mood this list does not have, zmood: is private by section 4.9.

Closed groups

A group holds a keypair and is addressed by the X5 callsign derived from it. The admin holds that key; handing it over is the whole of succession. The group announces itself with t:identity and names itself with nick:.

t:moderate f:X5A3F2 d:X5A3F2 ts:... grant:X1RD89,X32DVA sig:...
t:moderate f:X5A3F2 d:X5A3F2 ts:... grant:X32DVA role:mod until:... sig:...
t:moderate f:X32DVA d:X5A3F2 ts:... revoke:X1PZ4Q until:... sig:...
t:moderate f:X32DVA d:X5A3F2 ts:... r:89a9c8 hide:message sig:...
t:moderate f:X5A3F2 d:X5A3F2 ts:... revoke:X32DVA since:... sig:...

A subgroup is an ordinary closed group with its own key, listed by another with grant:<X5> role:sub and delisted with revoke:. Listing confers no authority inside it and membership does not travel down. Five levels counting the root; a listing that makes a cycle is ignored.

f: signs, d: names the group. revoke: with until: is a suspension; revoke: with since: voids that moderator's acts from then. Only the admin appoints; a moderator may revoke and hide. Authority is judged at the act's ts:; newest per signer wins, identifier breaks a tie, a future ts: is discarded. until: on a grant is optional, revocations are kept. Asking to join is an ordinary message, there is no join packet.

Never filter sos, warning, info or replies to them. A client that cannot verify shows everything and marks the group unverified. Closed is not private: the roster and the whole moderation history are public, permanent and gatewayed, which is more exposure than an open group, not less.

Licensed spectrum

Licence-free and internet Licensed spectrum
f: any, self-generated X1-X5 included only a callsign issued to that operator
sig: permitted permitted
x: permitted, default on direct messages never on amateur bands
t:challenge works cannot: it seals a nonce

A self-generated callsign never goes on a licensed frequency, and a gateway must drop such packets rather than relay them under its operator's licence. Announce an issued callsign with t:identity under that callsign to bind it to a key; the signature proves key possession, and entitlement is checked against the authority's register, never in a packet. Signing is lawful on amateur bands because a detached signature leaves m: in clear; sealing is not.

near: is not rad:. rad: is the area a subject occupies; near: is how close to dest: is close enough. A warning carried to a town uses both.

dest: with no d: delivers to a region rather than a person; nothing is acknowledged.

Two copies by different routes share one identifier, so the recipient sees the message once and keeps both via: lists as routes that work.

Seal the body with x: and leave the routing keys in cleartext. Coarsen dest:, it geolocates your correspondent.

q:sign asks for a signed receipt; it copies the arrival via: into route: and signs it. s:sign without a valid sig: is discarded.

sig: goes on every packet by default, 65 bytes. Drop optional fields before dropping the signature. Not signed: challenge, response.

Identifiers

Never transmitted. Both ends compute sha256(the packet, with sig: and via: removed) and take the first 6 hexadecimal characters. r: carries an identifier when referring to another packet, including the sender's own when withdrawing it. Signing and relaying do not change it, which is why those two fields come out before hashing.

Limits

Thing Limit
packet, every transport 250 bytes
parts in one message or post 9, n:1/9 to n:9/9
titled post, inline about 1650 characters
relays, sos and warning 9
relays, everything else 3
incomplete set of parts held 10 minutes
challenge answered within 60 seconds
group name 1 to 16 characters, uppercase
callsign any length, uppercase
key 1 to 16 characters, lowercase letters and digits

Private use

Keys, and q: and s: words, beginning with z are never assigned by this document.


36. Reserved

Assigned packet types: message, observation, receipt, reaction, request, identity, track, sos, warning, info, challenge, response, blog, passage, event, offer, need, channel, mailbox, service, command, result, moderate, status, place, poll, file, report, ping, pong. All other lowercase words are reserved.

Assigned keys: t, f, d, ts, tz, q, s, r, n, via, track, seq, title, dest, onboard, price, cw, freq, bw, shift, urg, scope, lang, nick, hold, serve, cmd, arg, code, ver, url, owner, use, first, ssid, pass, wifi, ip, zone, ap, key, nsec, near, route, relay, tone, input, power, mode, ch, range, site, source, every, for, at, kind, sev, rad, tag, type, m, file, x, xr, sig, k, add, remove, grant, revoke, role, accept, leave, hide, mood, only, opt, vote, root, size, since, until, pos, alt, acc, spd, dir, o, climb, temp, hum, intemp, inhum, wave, swell, seatemp, vis, press, wind, wdir, gust, rain1, rain24, solar, batt, volt, rssi, snr, link, busy, txtime, hears, peers, mail, lx, uptime, lifetime, odometer, fw, state, level, target, dose, lifedose, radon, rf, efield, mfield, produces, consumes, grid, storage, charged, load, lifeproduces, lifeconsumes, lifegridin, lifegridout, lifeload, age, epoch, b, ih, ph, name, count, have, off.

Assigned q: and s: words: section 28.

Reserved prefix: z, for both keys and words.

A new field takes an unused key and inherits the skip-unknown rule. A new purpose takes an unused type. Neither redefines an existing assignment.


37. Implementation status

Element State
On the air implemented on the Flutter side: the discovery beacon is an XPRS t:observation on its own BLE5 subtype 0x58 (docs/ble5.md), carrying peers:, hears: and mail:; MeshCourier emits XPRS for every carried message, and custody reads it. The chat wapp emits XPRS too since 0.4.38, 1:1, group, broadcast and position (apps/chat/xprs.c, docs/aprs-xt.md section 2.2), keeping the compact frame only for its own control frames (?MAIL, ?IGATE, ?PING) and for a body over 250 bytes. Every receiver still reads both, so an un-ported peer keeps working. The ESP32 T-Dongle (esp32/rns_ble5) speaks XPRS too via esp32/components/geogram_xprs, a C mirror of lib/services/xprs/ replaying the same 205-example corpus: it reads both subtypes, parks t:message mail by derived identifier, relays with via:, answers t:ping with t:pong rssi: (rate-limited per section 22.2), refuses scope:local at custody admission, dates its packets epoch: (section 15.7, NVS boot counter) and transmits unsigned
The packet format itself implemented; lib/services/xprs/ parses, encodes, derives identifiers and signs. Every example packet in this document is a test fixture: test/xprs_packet_test.dart round-trips all 201 byte-exact, checks each stated byte count, and cross-checks every identifier against an independent Python implementation
Section 5 identifiers implemented
Section 6.1 signatures, and surviving a relay implemented; test/xprs_sig_test.dart signs, relays three hops and re-verifies
Section 11.9 station ownership and owner policy implemented on the station: an unowned ESP32 accepts the first verified, uncarried cmd:set owner: naming its own sender and writes the key into its allow-list; an owner sets use:, first: and serve: the same way, each answered with all four as they now stand; use: is enforced at the door a phone hands a packet through, with t:sos and t:warning exempt; first: ranks the queue through a hook the bearer takes; a policy command not strictly newer than the last accepted is 408; q:policy is answered to anybody. An unowned station airs q:owner scope:local with its k: on its local bearers, first a quarter of a minute in, then every two minutes, and on Bluetooth every half minute, signed, and stops on being claimed; a claim carrying the claimer's k: is verified against it. The codec side is in lib/services/xprs/xprs_station_policy.dart, and the app's Firmwares wapp offers the claim
Section 11.10 setting up a station (ssid: and pass: sealed, wifi:, nick:, zone:, ap:, key:new, nsec:; the claim and the ask carrying k:) implemented on the ESP32 station (common/xprs_app: cmdset_apply, setup_apply, sealed bodies opened by common/xprs_sig/xprsseal.c after the signature and the allow-list), in the app core (a $type:"secret" field the host never stores, commands never deposited with an archiver, a station's key learned from its ask) and in the Firmwares wapp (apps/firmwares). Bench-validated on an ESP32-C3 and an Android phone over the station's hotspot: claim, stats, name and zone, a sealed network joined in a second; then, with the station on the network it was given, a new key followed through the restart. The Bluetooth leg was shown on a T-Dongle-S3 (2026-09-12): the phone's claim, name, stats and a sealed WiFi password each answered on the first airing at -49 dBm. The same station was set up from a desktop over the LAN, with a wrong password answered 500 m:wrong password from the network the station went back to, and a key:new followed through the restart. The ESP32-C3 hears that phone at -96 to -100 dBm and decoded none of its frames; it is set up over its hotspot. A station that answers over the LAN also sends the answer straight to the address the command came from, because its broadcasts stopped reaching a phone on its hotspot once it had joined another network
Section 9.1 relay budget, 9.2 loop check implemented in the codec (xprsMayRelay, xprsWouldLoop); nothing transmits via: yet, so nothing calls them on the air
Section 9.11.3, scope:local is never carried implemented; refused at custody admission in MeshCustodyDelegate
Section 12.8.1 automated return leg (release on hearing, forward toward gossip) implemented on the Flutter node (funnel-triggered release, XprsForwarder with via: and the loop check) and in the shared ESP32 app (release-on-hearing off the seen funnel, paced re-air, receipt purge); the T-Dongle keeps its original blemesh_scf_* loop. Bench-validated end to end. Since 2026-10-25 the Flutter forwarder sends to EVERY gateway with a fresh live sighting (one copy each, remembered per gateway), re-runs when gossip gains a new gateway for a held recipient (XprsGossip.onSighting), and forwards nothing for a recipient it can reach itself; the ESP32 archiver still picks one gateway. Proven on the three-station bench (test/xprs_reach_anywhere_sim_test.dart): a BLE-only station reached from the internet through the phone that reported hearing it, two reporters both served and released by one receipt, mail deposited before any report moving when the report arrives
Section 12.9.4 gossip (layers, budgets, always-on archivers) implemented on the Flutter side (xprs_gossip.dart: L2/L3 tables, K/G caps, signer quotas, byte budget, the always-on-archiver mode and the miss-path ask) with DoS-probe unit tests; the shared ESP32 app keeps the need-to-know ring. Always-on is a LOCAL budget setting in both (index_always_on on a station, Always on in the app), never announced: each side reads a peer's depth off count: and uptime: instead, and both stopped airing and parsing serve:archive,super in 2026-09
Section 12.12 reaching a callsign from anywhere implemented and bench-validated: an internet sender on a foreign network reached a WiFi-less BLE-only pocket through deposit, gossip, forward and release-on-hearing, with signatures intact at every hop
Section 12.12.1 constrained internet transports (directed replies, directed asks, deposit, sender-parks-own-mail) implemented on the Flutter side; specified after being proven necessary on public hubs
Section 12.10.2 the poll adapts to what it finds (per-archiver interval, the quiet ladder, the peer's floor, 429 as authority) implemented on the Flutter side, with the ladder as pure functions under unit test; bench-measured 600s down to the 15s floor under load and back up to the ceiling when the room went quiet
Section 11.2.1 a continuation must make progress specified in this revision after both halves failed on the bench; the asker's half (a stalled resume is not news) is implemented on the Flutter side, the responder's until: half is not yet
Section 12.9.4 what an always-on archiver must be implemented as a setting, not a role: the Flutter app's Always on is a further promise a station opts into, and it can only be made by a station that is already a public archiver. Nothing about it goes on the wire — a peer decides which archivers are worth leaning on from serve:archive plus count:, uptime: and whether the station is addressable off-radio (xprsLooksAlwaysOn). The app used to air serve:archive,super, a word this vocabulary never had, and now neither airs nor parses it
Section 12.12.2 public traffic across the internet (push to chosen archivers, pull from them, publications past the declaration rule, callsign-to-address resolution) implemented and bench-validated: two phones on different networks, neither hearing the other's broadcasts, exchanged Global chat through one always-on archiver, push arrived in seconds, pull on the metering period
Callsigns, signatures, verification implemented
Signing by default on every packet type not implemented; signing exists and is opt-in
Direct, group and broadcast messages implemented
Replies and reactions implemented
Receipts and carrier release implemented, for receipts that were asked for with q:
Section 12.7, an archiver holding mail implemented (XprsMailbox): a station deposits with the archivers it named, the archiver holds what has no receipt and re-offers it when the recipient reappears, and both ends send receipts to the sender's declared holder so it stops. MeshStore custody and the LXMF propagation mailbox remain the neighbouring mechanisms for their own lanes
Section 36, publishing to chosen archivers specified, not implemented. Every piece it is built from exists, the signed-record discipline, the append-only log with an (epoch, seq) cursor, archiver-to-archiver catch-up and archivers as the DHT's anchors are all live for FILES (files/dht/, social/relay_node.dart), but nothing yet keeps a publication log, pushes packets to a chosen archiver, or answers a query from their fields. The section deliberately adds no packet type and no key, so there is nothing on the wire to implement: the work is all plumbing
Section 12.1, gateway reachability publications (observation/identity to an archiver) implemented as a push on both reference stations: the ESP32 station deposits one signed observation per turn with its archivers (air_signed_observations), and the Flutter node deposits one per radio (XprsHearsReporter, MeshService._reportHeard) at once when a station its last report did not name is heard, else every five minutes, with its t:identity every half hour so the archiver can verify the claim. The archiver feeds a verified claim into its gossip off the internet lane and routes by it (12.8.1)
Section 12.6, only: matching inside list fields partly implemented: the shipped history responder matches only: against author, addressee and, for observations, the hears: list (xprs_archive.dart query); hold:/via:/grant: containment is not searched yet. The asking side is automated since 2026-10-25 (XprsFetch): a wapp reading one station's messages makes the core ask the archiver that answered for it last, then the chosen ones, following a 404 m:try one hop and a 206 a few pages; the rows that answer are kept on the followed shelf ("what you asked for, you keep"), which is what lets a pocket phone hold a stranger's public words it went looking for
Section 12.8, sealed-mail release to a hearing gateway specified, not implemented; the nearest live relatives are the chat iGate mailbox (mail pulled from APRS-IS by an in-range station) and MeshStore custody, neither of which is driven by a published hears: claim
Section 12.9, serve:archive and the XDIR1 directory exchange partly implemented: serve:archive is announced and read, and a miss answers code:404 m:try naming the gateways gossip knows HEARD the callsign (the Flutter and ESP32 history responders both). The XDIR1 directory itself is not built, so m:try names who heard a station rather than who KEEPS its publications, and an asker following it one hop reaches a hearer that may hold nothing. pointer_sync.dart gossips signed ADDRESSES between file-archivers and re-verifies on merge, never copying content
Section 6.2.1, redacted packets (xr:) implemented: the composer parses ((...)), derives the slow key and builds the bars and the xr: blob in the core (XprsCrypto.redact/restore, hal_xprs_redact/hal_xprs_unlock), and a reader opens one at a tap, the passphrases that worked being remembered in the profile's encrypted store. Chat composes a redacted message in a 1:1, a closed group and the Local room; the wire is addressed there and scoped (scope:local) here. What is not implemented is redaction of the FIELDS the table permits (pos:, telemetry, title:): only m: is marked today
Section 14.7, working-channel invitations specified, not implemented as packets; the dance itself ships in binary for one pair of bearers, the WiFi-Direct negotiation (BLE subtype 0x57 ADVERT/REQ/OFFER, docs/ble5.md) coordinates exactly this move from the shared advert channel to a private fast lane, and 14.7 is that handshake generalised to every bearer, in text, signed
Section 3.1, one person on several devices specified, not implemented. Nothing numbers a device today: a station wears its bare callsign, and the chat wapp matches d: against that alone. The pieces the rule needs are already on the air, a beacon carries f: and lx:, so a device can see a sibling and tell it apart, but no code adopts a suffix, prefers the conventional number for its type:, or refuses a command addressed to a person
Section 9.7.1, receipts signed by default specified, not implemented. Signing exists (section 7.1) and receipts do not use it yet, which leaves the forged-s:ack deletion described there open on any station that honours the section 8 carrier release. The Reticulum side is not exposed to it, its acknowledgement is a link, not an XPRS packet, but an XPRS-native carrier would be
Section 9.7.2, parking a retry with no evidence implemented on the Reticulum side: a retry is spent only against a live path or a beacon heard in the last three minutes (RnsService._peerReachable), otherwise the entry parks without burning a rung and the copy stays held
Section 9.7.1, receipts without asking specified, not yet on the air. The rule and its exclusions are settled and the two example packets are test fixtures; no station sends an unasked s:ack yet. What made it necessary is fixed already on the Reticulum side: an unacknowledged single-packet delivery no longer reports itself as delivered, and the sender retries at 20s/60s/5min before leaving the copy held (lxmf_router.dart)
Long messages in parts implemented
Encryption and the sealed-body band rule implemented
Sections 3.2, 9.11.5, 14.8, the LoRa modes and the channel shared with Meshtastic implemented in the ESP32 firmware (common/xprs_meshtastic, docs/lora.md): LongFast on Meshtastic's frequency, XPRS in its own frames, Meshtastic relayed by Meshtastic's rules, and messages, direct messages (Meshtastic's public-key form), replies, likes and names translated both ways. Bench-validated 2026-09-19 against a stock Meshtastic 2.7.26 node and the Meshtastic Android app; a gateway delivers only to nodes it heard itself. The LoRa mode (xprs, meshtastic or meshcore) is switched live, with no restart, by config, on the T-Deck's screen, from the serial console or by an owner's cmd:set lora:; a station can also survey every mode in turn and report who is on each. The first two modes and the live switch are verified on the bench; the P1-Pro (nRF52, xprs mode only) is heard again by stations in xprs mode. meshcore mode carries XPRS on MeshCore's channel, relays MeshCore's own traffic by its flood rules and translates messages both ways, adverts and direct messages included (common/xprs_meshcore, host-tested against the published layouts, against OpenSSL and against a simulated MeshCore node); Bench-validated 2026-09-20 against a node running MeshCore v1.17.1: an XPRS callsign appears in its contact list, channel messages and direct messages cross both ways, and a direct message is acknowledged into a signed gateway receipt. Its repeaters do not carry XPRS's own frames, so XPRS on that channel is direct-range only. On that side a channel message is unsigned and names its sender only by a name, so one crosses only when an advert this station heard carries that name. The remote switch is host-tested, not yet on the bench
Section 6.4.1, no self-generated callsign onto licensed spectrum implemented in the firmware: the SA818 transmitter on the kv4p refuses a frame from an X1 to X5 sender, or from a callsign of another network (section 3.2), at the one function every web-chat, console and BLE-bridged frame goes through, and the web chat says why; the APRS-IS iGate logs in receive-only (passcode -1) until an issued callsign is set (POST /api/igate/callsign), and never gates up a frame from an X1 to X5 sender. xprs_is_self_generated and xprs_is_unissued in common/xprs_codec decide both. The app and the chat wapp no longer connect to APRS-IS at all
Section 6.4.2, an issued callsign bound to a key by t:identity implemented in the app: an operator enters the callsign on their licence in the profile editor, every packet this station originates then carries it in f:, and publishIdentity airs the self-signed binding at once rather than waiting for the 30-minute cadence (29.1). The receive side already bound such an announcement, because it checks the self-signature against the k: it carries and never the derivation. A peer's issued callsign is now believed on that binding too, where before it was silently replaced by the X1 its key derives. The app keeps onlyIfUnknown, so the first speaker for a callsign wins, and it is a record each node keeps and never a verdict any node transmits: nothing claims a callsign is licensed, on the wire or on the screen
File references by content hash implemented, in the base64url form this document now specifies (MediaRef); the older 64-hex form is still read
Identity announcement implemented
key:value fields separated by spaces implemented everywhere the device transmits: the beacon, carried mail, and the chat wapp since 0.4.38. The three-0x1F-field frame remains only as a fallback that is still read, and that chat still sends for its control frames
t: packet type as the first field implemented; chat's own routing (a callsign, a #group, ! for a position, empty for in-range) is now expressed as t: plus d:, not inferred from one overloaded field
Derived identifiers not implemented; the current wire hashes message content without a timestamp, so every OK collides, and carries a separate receipt identifier
ts: on messages implemented on the chat wire since 0.4.38: a message now states when it was written, so one that waited in a mailbox no longer shows the minute it arrived. A packet with no ts: is still read (section 15.7)
Sections 11.7 and 11.7.2, a device reporting implemented in the doorbell firmware (firmware/models/reolink-d340w), the first X4 device in this project that holds its own key rather than a controller holding it for it. The daemon on the camera signs and airs state:pressed, state:motion and state:clear with a url: to a still it serves itself, over the LAN, and it serves that still at API-HTTP.md's /door/snapshot.jpg and /door/stream.mjpeg by proxying the camera's own API so no credential is in a URL it hands out. The reporting half only: it answers nothing, because q: needs a listening socket it does not open. The phone reads these in the Things wapp (apps/things), which works out what a thing is from the state: word it used and the keys it sends, there being no capability field in this format to ask for
q: and s: not implemented; receipts exist, requests do not
via: instead of rewriting f: not implemented; a carrier currently retransmits under its own callsign, which breaks both authorship and the identifier
Relay limit by packet type not implemented; custody re-airs a fixed three times with no path recorded
t:track tracks not implemented; no track is recorded or published
t:sos calls for help not implemented; the current wire has an sos station symbol, which is a different thing and is not relayed further than any other packet
t:warning warnings not implemented; no source
t:info notices not implemented; no source
t:blog posts not implemented
wave, swell, seatemp, vis not implemented; no source
t:passage, t:event, t:offer, t:need not implemented
price: not implemented
cw: content warnings not implemented
t:channel not implemented
Carrying toward a place (dest: on a message) not implemented; custody currently carries only to a known callsign
urg: not implemented
scope: local is enforced on the sending bearer by the app and the station firmware; regional, the arrival check and the unknown-word rule are specified and not yet implemented
lang: not implemented
nick: and signed identity not implemented; identity is announced unsigned today
t:mailbox implemented: a receiving station records verified declarations (windows and remove:mailbox included, lib/services/xprs/xprs_archive.dart), and a station now COMPOSES one naming its own archivers whenever that list changes, which is what lets a recipient send its receipt to the sender's holder (12.8.3); custody consults the declaration when choosing where to hand mail next
Several mailboxes with windows, and cancellation implemented on the receiving side (see t:mailbox above); a composer states one open-ended declaration, not several windowed ones
t:service implemented: the discovery beacon carries a signed t:service with the station's serve: claim, and a receiving station records it (XprsMonitor), which is what the archiver, relay and role filters read
t:command and t:result implemented for cmd:history: the phone and desktop host answer a command addressed to them with signed results (lib/services/xprs/xprs_history_server.dart); no other command is acted on yet
cmd:history, backfill by replay implemented: every device keeps a spool by default (500 MB or a year, the owner's numbers per section 30.3) and re-airs the original packets newest first on request, code:202, the page, then code:200 or code:206, metered per section 22.2 and paced for the advert channel
cmd:file, fetching bytes by hash implemented: a station answers a signed cmd:file for any hash it holds, gated by the audience the reference was shared with (lib/services/xprs/xprs_file_server.dart, xprs_file_acl.dart), re-sending only what a have: map lacks over the packet lane and bracketing the bulk lane with t:result; the resolution ladder underneath it (Reticulum direct, DHT, LAN, I2P, BitTorrent) is what the fetcher falls back to when no station answers
Section 11.8 unattended: a station finding and installing its own next firmware implemented on the ESP32 station (2026-10-25, common/xprs_fwfetch, docs/plan-firmware-folders.md): with fwauto on it asks an archiver cmd:history kind:file only:<publisher> every six hours, takes the newest <board>-<version>.bin tagged with its channel whose description verifies against the pinned fwkey, fetches it with cmd:file over a Reticulum link and resource streamed into the OTA slot (common/xprs_rns), and installs under 11.8's rollback. The link and resource receiver are checked against reticulum-dart's own file server on the host; not yet bench-tested end to end. Every XPRS app keeps the released images and relays the publisher's signed descriptions verbatim to its archivers (the app's FirmwareMirrorService, on by default), so the archiver a station asks holds the answer and the bytes; a station with storage keeping and serving copies (12.9.2) is not written
cmd:put, depositing bytes specified, not implemented as a command; the machinery exists as the Reticulum deposit session (FileDepositSession) and the MSP bulk lane's accept-at-size handshake, what is missing is the XPRS ask in front of them
q:have / s:have, who holds a file implemented: a holder answers have:full from its store, a miss names other holders from the sources index; the mid-transfer have: map of 7.7.6 rides cmd:file
Listings (XFL1), folders as snapshots implemented as the packet-layer form of the shipped folders lane (2026-10-25): the app builds the deterministic XFL1 listing, stores and holds it, and publishes the signed t:file kind:folder with one signed t:file per member, tag: naming the channel, deposited with the archivers (lib/services/xprs/xprs_folder.dart, POST /api/xprs/folder/publish); the ESP32 station reads listings and descriptions (common/xprs_folder) and fetches a member it wants with cmd:file over a Reticulum link and resource it now speaks (common/xprs_rns/rns_link.c, rns_resource.c, streamed into the OTA slot). The live op-log folders remain the mutable lane; a snapshot is published from one on demand
Inline files in b: 7.7.6 implemented: chunks by offset as datagrams, the receiver's have: map, the holder's re-send of the gaps; the 7.7.4 nine-part n:X/Y split of b: is specified, not implemented
A file in a conversation (7.7.7) implemented: the reference is lifted out of the caption into file: with size: and name:, a picture too large for the packet lane travels as a preview plus a t:file r: describing the original, and automatic fetching is by bearer -- packet-lane sized on sight, larger only where a link that is not a shared radio can carry it
Deterministic torrents, ih: derivable implemented (lib/services/torrent_service.dart builds byte-identical torrents from content, so every holder derives one infohash)
serve:archive implemented, and announced only by a public archiver: every device spools its own traffic and that of the callsigns it follows, which needs no claim and makes none. The claim goes on the air when the operator opts into keeping strangers' packets — the default on a powered machine, off in a pocket (12: silence is not consent) — and dropping it drops the answers to strangers with it, leaving the station's own publications still served
Section 13.4, one port for Reticulum and XPRS implemented on the TCP hub listener: port 4242 answers HDLC-framed Reticulum and line-oriented XPRS on one socket, told apart by the first byte
Retention policy, and keeping by worth rather than by age deliberately unspecified (section 30.3); the shipping custody store bounds itself at 100 MB or 7 days and evicts ORDER BY urg, ts, which is exactly the kind of local decision this format leaves alone
Paged replies, code:206 implemented by the history responder: twelve packets a page over the air, the probe row deciding 206 against 200, and the requester continuing by moving until:
t:poll and vote: not implemented; nothing puts a question or counts an answer
@CALLSIGN mentions not implemented; no wapp scans m: for them and nothing notifies on being named
root: on a reply not implemented; the chat wapp threads by parent pointer only (docs/aprs-xt.md), so a lost middle orphans the rest
t:file and size: (now also name:, ph:, kind:folder, count:) implemented as a packet on the publishing side (2026-10-25): the app composes signed member and folder descriptions with size:, name:, tag: and kind:folder/count: (xprsDescribeMember, xprsDescribeFolder), and the ESP32 station reads them (xfolder_desc_member, xfolder_desc_folder); ph: is not yet written. The NOSTR kind-1063 metadata remains the app's own index
link:, busy: and txtime: not implemented; the Reticulum side tracks announce cadence and the LoRa drivers know the duty cycle, but nothing measures or publishes channel occupancy
hears: implemented: every phone beacon (BLE and LAN) and every ESP32 beacon carries who the station heard directly, signed, cut to the bearer's budget with peers: the true total (15.6.4), and the same claim is deposited with the station's archivers (12.1). An earlier note here said the opposite; it described hal_rns_nodes, which lists Reticulum announces and is not this field
t:report not implemented; the chat wapp has moderation ops but no way for an ordinary station to flag anything
add:repost not implemented; the chat wapp has reactions (add:like) and no repost
t:place not implemented; nothing in the codebase reports a thing that is not the sender
Avatar and description on t:identity implemented: nick:, file: and m: from the profile, - to clear, split at 184 bytes; aired on a change and once a day, deposited with the archivers; receivers keep the newest value per field and fetch the picture with cmd:file for the stations they know or are showing. A NOSTR kind-0 note carries the same for NOSTR clients
Section 31, airtime not implemented as stated here, though the Reticulum side has real cadences (30 s charging, 5 min on battery) and the NOSTR side has stranger-serving budgets
t:status not implemented; the Social wapp has a feed, but it is NOSTR kind-1 notes over the internet and Reticulum rather than XPRS packets (docs/social.md)
mood: and client theming not implemented; nothing reads a mood and no client changes appearance for one
X5 group callsigns and t:moderate not implemented; groups are plain names with no member list anywhere
Subgroups, role:sub nested five levels deep not implemented; the chat wapp has a sub-room tree, but on NOSTR rooms and with authority inherited down the tree rather than stopping at each key
Never filtering sos, warning and info by membership not implemented; there is no membership filter to exempt them from
The chat wapp's own moderation implemented, and by a different design: NIP-72 rooms with a NOSTR kind-9078 op-log, authority from the room event's author rather than a group keypair, and no roster at all (docs/chat-rooms.md). Nothing in section 10 is built, and reconciling the two is not attempted here
cmd:interpret not implemented; no station interprets natural language
near:, regional delivery, route: in a receipt not implemented
q:sign and signed receipts not implemented
Recurring windows, site:, source:, range: not implemented
Section 15.5.2 energy readings partly; the app keeps the latest energy readings a station reports and shows them in its station panel. Nothing sends them yet
t:challenge and t:response not implemented; no challenge exists, and a spoofed authority-issued callsign is currently undetectable
Periodic t:identity implemented; self-signed, aired on every active bearer at start and every 30 minutes (29.1), and q:identity is answered directly
since: and until: not implemented; nothing in the current wire carries an event duration, and nothing expires on its own
type: vehicle set partly; the current wire carries a handful of symbols and none of the rail, air or cycle values
Variable-length and authority-issued callsigns implemented in the app: one grammar for the shape of a callsign (lib/services/xprs/xprs_callsign.dart), which four files used to answer four ways, and which now carries the / suffix that one of them was missing, so a station announcing G0XYZ/P is named rather than heard and left anonymous. The ceiling is fifteen characters, set by the narrowest hop (a wapp reads hal_identity into sixteen bytes), and the MSP custody protocol was widened from nine to match. A callsign too wide for a frame is now REFUSED rather than truncated: a truncated callsign is not a shortened name, it is a different station, and MSP was filing carried mail under one. Still not implemented: nothing numbers a device (3.1), and t:challenge/t:response remain unbuilt, so a spoofed authority-issued callsign stays undetectable
pos: coordinates implemented in a different encoding
age: and epoch: time not implemented; requires an epoch counter in non-volatile storage
alt, acc, spd, dir, o, climb movement not implemented; the platform supplies these values and the location layer currently retains only latitude and longitude
temp, hum weather one hardware sensor exists and reaches a local display only
intemp, inhum weather not implemented; the one sensor that exists is indoors and is reported as if it were outdoors
press, wind, wdir, gust, rain1, rain24, solar weather no source
batt, volt telemetry not implemented; charging state is tracked, charge level is not
rssi, snr telemetry implemented on the receive paths
uptime, lifetime telemetry implemented on the ESP32 T-Dongle beacon (esp32/rns_ble5, lifetime accumulated in NVS); the phone beacon does not carry them yet
dose, lifedose, radon, rf, efield, mfield radiation (section 15.5.1), odometer specified; no shipping station has a sensor for any of them yet

Credits

XPRS is written by Max Brito. Copyright (c) Max Brito and XPRS contributors, licensed under the BSD 3-Clause License: share and adapt it for any purpose, including commercially, keeping the copyright notice and the licence text. Full text in LICENSE, notice in NOTICE. Implementations are separate works under their own licences.