Way 1 — direct: pair it with your wallet
Works with any gateway whose packet forwarder can send to more than one server (Crankk, RAK, Dragino, MikroTik, most Helium-era hotspots).
- In the gateway's packet forwarder add a server:
sensmos.com, port1700(UDP, uplink and downlink). Keep your other servers — the forwarder sends every frame to each of them. - Wait a minute, until the gateway has sent us something.
- In the Sensmos app: Dashboard → + → Add LoRaWAN gateway. Enter the gateway's EUI (16 hex characters, from its panel), optionally a name and a position — or use your phone's position while standing next to it. Tap Pair with wallet.
The app refuses if the gateway has been silent for more than 10 minutes (check the server entry) or is already paired with another wallet.
What you get: a card in the Dashboard — how many nodes the gateway hears and how many hear it (24 h), Sensmos frames heard in 24 h, the last beacon, earnings. On the live map the gateway has its own colour and panel. Name and position edits it; Unpair gateway on the card releases it.
Until it is paired: a gateway that already forwards to sensmos.com:1700 for 10 minutes starts working for the network under the project's wallet — its Sensmos frames are delivered, it transmits the coverage beacon and counts in the epoch. Pairing moves the card and the earnings to you; a gateway nobody pairs keeps earning for the project.
What the network does with it: every Sensmos frame it hears — emergency broadcasts, sensor frames — is delivered to the owner exactly like a frame heard by a node. Every 5 minutes the network transmits a short coverage beacon through your gateway; nodes that hear it confirm the gateway is where it says it is.
Way 2 — passive: one script, nothing on the gateway changes
For a gateway you do not want to reconfigure. A Python script on the gateway reads the forwarder's traffic on loopback and reports radio metadata. It needs one Sensmos node on the same LAN to vouch for it. Details follow.
Who this is for
Any Linux box running a plain Semtech packet forwarder (UDP, port 1700-style) pointed at a remote server: Helium hotspots and their multi-mining stacks, RAK, Dragino, Mikrotik, a Raspberry Pi with a concentrator HAT. If your gateway runs ChirpStack Gateway Bridge instead, there is a local MQTT broker and a different agent applies.
You also need one Sensmos node on the same LAN as the gateway (any ESP32 with the firmware, no radio required) and its PIN. The node vouches for the gateway; without it the backend will not issue a token.
Why passive, not a relay
The obvious way to tap a forwarder is to put something in the middle — point the forwarder at us and forward onwards. We deliberately do not. On hardware that earns, our outage would become your outage. The forwarder already sends every frame to each configured server separately over loopback; the script just reads along on lo. It is nobody's target, relays nothing and delays nothing.
Quick start
Run all of this on the gateway, not on your laptop. Python 3 only — no pip packages, nothing to build. Root is needed because reading raw packets requires CAP_NET_RAW.
# 1 — install the script
sudo curl -fsSL -o /usr/local/bin/sensmos-gw-udp.py https://raw.githubusercontent.com/Galusz/sensmos-gateway/main/sensmos-gw-udp.py
sudo chmod +x /usr/local/bin/sensmos-gw-udp.py
# 2 — see what would leave your box (sends nothing, needs no pairing)
sudo sensmos-gw-udp.py --dry-run
# 3 — find a Sensmos node on this network (prints the exact pair command)
sudo sensmos-gw-udp.py --find
# 4 — pair with it (address from step 3, PIN = the node's PIN)
sudo sensmos-gw-udp.py --pair NODE_IP --pin YOUR_PIN --name "rooftop RAK"
# 5 — install & start it as a boot-persistent service
sudo sensmos-gw-udp.py --install-service
# 6 — confirm
systemctl status sensmos-gw-udp --no-pager
journalctl -u sensmos-gw-udp -f
Step 6 should show active (running), and the log prints connection works — first batch accepted once frames flow, then a status line every minute. In the app, the gateway appears under the node's Attachments; on the live map the node gains the RF layer.
Already paired, no service? The token survives; re-run steps 1 and 5. Only if --install-service says pair first repeat step 4.
Look before you trust us
--dry-run prints, as JSON, exactly the frames it would forward and stops there. Add --log-tx to also see when your gateway transmits. The script is a single readable file; the source is on GitHub.
Pairing — how the token works
The script has no credentials of its own. It asks the Sensmos node on your LAN (with the node's PIN) to vouch for it; the node relays the request to the backend over its own link, and the backend issues a token scoped to radio.frames — reporting received frames, nothing else. The token is stored in /etc/sensmos-gw-udp.json (mode 0600). From then on the script talks to the backend directly; the node is out of the loop.
- One node can vouch for up to four attachments.
- The owner can revoke any of them from the app (node → Attachments); the script becomes inert immediately.
- A token lives 30 days from first use and the script renews it by itself.
The full contract (endpoints, scopes, formats) is on Attachments & the EXT API.
What it reads
Semtech GWMP over UDP, recognised by packet header, not by port, so it works whatever ports your stack uses:
| byte | |
|---|---|
| 0 | protocol version (1 or 2) |
| 1–2 | token |
| 3 | type — 0 PUSH_DATA (frames received), 3 PULL_RESP (gateway asked to transmit) |
| 4–11 | gateway EUI |
| rest | JSON |
From PUSH_DATA it takes the rxpk entries: frequency, spreading factor, RSSI, SNR, size, CRC result and the raw PHY payload. Application payloads are encrypted with keys we do not have; what the network uses is the radio metadata and recognising Sensmos frames among the traffic.
PULL_RESP — a transmission your gateway is about to make — is reported only when it is audible to others (ipol = false, e.g. coverage beacons some networks transmit). A normal LoRaWAN downlink uses inverted IQ that nobody else can hear, proves nothing, and is skipped. Reported ones are proof the gateway is real: their payload is unpredictable, so a Sensmos node that hears the same bytes independently confirms the gateway actually transmitted.
A multi-server forwarder sends the identical frame to every configured server, so the same transmission shows up several times on loopback (RF packets received: 1 / PUSH_DATA datagrams sent: 2 in your forwarder log). The script deduplicates; one transmission is reported once. Frames are batched (once a second or every 50 frames) and sent to https://sensmos.com/v1/ext/radio/frames.
Flags
| flag | meaning |
|---|---|
--iface | interface to watch, default lo |
--dry-run | print what would be sent, send nothing |
--log-tx | also log what your gateway transmits |
--find | scan the LAN for Sensmos nodes and print the pair command |
--pair IP --pin PIN --name "…" | one-time pairing through a node |
--install-service | copy to /usr/local/bin, write and enable the systemd unit (shown to you first) |
--be URL | alternative backend, for testing |
The unit it writes, if you would rather do it by hand:
# /etc/systemd/system/sensmos-gw-udp.service
[Unit]
Description=Sensmos passive gateway listener
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/bin/python3 /usr/local/bin/sensmos-gw-udp.py
Restart=always
RestartSec=10
User=root
[Install]
WantedBy=multi-user.target
Docker and containers
The script sniffs AF_PACKET on the host's loopback. Loopback is private per network namespace, so an isolated container sees an empty lo and hears nothing. Running it in a container needs --network host and --cap-add NET_RAW. The supported way is the systemd service on the host; a container build is yours to keep working.
Updates
The script never updates itself — by design. It runs as root on your gateway, in your LAN; auto-pulling code from a server would make that server a way into your network. To update, repeat step 1 and restart the service:
sudo curl -fsSL -o /usr/local/bin/sensmos-gw-udp.py https://raw.githubusercontent.com/Galusz/sensmos-gateway/main/sensmos-gw-udp.py
sudo systemctl restart sensmos-gw-udp
Troubleshooting
root required: run with sudo— raw capture needsCAP_NET_RAW. Usesudo(orsetcap cap_net_raw+epon the Python binary, which grants it to every Python program —sudois cleaner).- Connected but no frames — check that traffic really is on loopback:
sudo tcpdump -i lo -n -c 10 udp. If your forwarder talks to a remote server over the network rather than to a local bridge, point the script at that interface:--iface eth0. AF_PACKET is Linux only— it is. This runs on the gateway, not on a Mac.--findfinds nothing — the node must be on the same LAN segment and online (its/infomust answer withws_connected: true). Give it the node's IP from the app instead.pair firston--install-service— pairing did not complete; check the PIN and that the node is online.
Uninstall
sudo systemctl disable --now sensmos-gw-udp
sudo rm /etc/systemd/system/sensmos-gw-udp.service /usr/local/bin/sensmos-gw-udp.py /etc/sensmos-gw-udp.json
Revoke the attachment in the app too. Nothing else was ever changed, so there is nothing else to undo.
In the network today
The first foreign gateway joined in August 2026 (Germany), vouched for by a node without a radio of its own. Frames it hears — emergency broadcasts and sensor frames of nodes in range — are delivered like frames heard by any node. Details of what LoRa does for a node owner: LoRa.