Yes: a MicroPythonOS badge can take part in end-to-end encrypted group chats over Nostr today, using the same NIP-17 "chat room" scheme that 0xchat and Amethyst use, and about 80% of the code is already in its Nostr Chat app. What is missing is a way to start a group from the badge and a handful of spec details that other clients rely on. The stronger alternative, MLS over Nostr (Marmot), needs cryptography and code that do not exist for microcontrollers yet.
Where MicroPythonOS stands today#
The Nostr Chat app already holds most of a private-group implementation; it only lacks a way to start a group. MicroPythonOS is an open-source operating system for ESP32 boards (240 MHz, 8 MB PSRAM) written in MicroPython, and its chat app runs on a hand-held badge with a small touch screen.
| Piece | State today |
|---|---|
| Direct messages | NIP-04, and NIP-17 (gift-wrapped, NIP-44 encrypted) |
| Public groups | NIP-28 channels, the "group chat" users see now |
| Private groups | NIP-17 rooms work on the wire: the sender can address several members with a subject, incoming messages are grouped by their member set, and replies go to every member. There is no screen to start one, so the badge can only join rooms that other clients open. |
| Relay authentication (NIP-42) | Not implemented, so inbox relays that gate private messages behind AUTH cannot be read |
The gaps that matter for talking to other clients are listed under Gaps found in the current implementation.
The options on Nostr#
Three schemes exist, and only NIP-17 rooms are both end-to-end encrypted and usable from a microcontroller today.
| NIP-17 rooms | NIP-29 relay groups | Marmot (MLS over Nostr) | |
|---|---|---|---|
| How it works | One message per room, sealed and gift-wrapped once per member with NIP-44 | Plain signed events on one relay that enforces membership | MLS group keys; messages encrypted under a per-epoch key |
| End-to-end encrypted | Yes, pairwise; relays see only wrapped events from throwaway keys | No: the relay operator reads content, senders and membership | Yes |
| Forward secrecy | No: a leaked key decrypts past and future messages | Not applicable | Yes, plus post-compromise security |
| Membership control | None: the member set is the room; changing it starts a new room | Admins, invites, kicks and roles, enforced by the relay | Admins, add and remove through MLS commits |
| Group size | Spec advises at most 10 members; 0xchat reports under 100 | Large | Large; welcome messages grow with the group |
| Works today with | 0xchat, Amethyst, Coracle, Snort; Damus in draft; Primal only NIP-04 | 0xchat, Amethyst, Flotilla, groups.nip29.com; the reference relay was archived in April 2026 | White Noise only; 0xchat and Amethyst in beta |
| Fits a microcontroller | Yes, with the cryptography already on board | Yes, the cheapest: one signature per message | No: needs X25519, Ed25519, AES-GCM, HPKE and a full MLS state machine, none of which exist for MicroPython |
Older tricks do not help: NIP-04 has no multi-recipient form and Amethyst stopped sending it in March 2026, and the per-client schemes of Keychat and Iris do not interoperate with anything else.
How a NIP-17 room message travels#

A room message starts as an unsigned "rumor" (kind 14) that carries the text, one p tag per member and an optional subject. For every member, the sender signs a seal (kind 13) around it and wraps that seal in a gift wrap (kind 1059) addressed to that member, signed by a fresh throwaway key with a randomised timestamp. The sender also makes a wrap for their own key, so their other devices see the message. Relays store the wraps and see only random keys and who receives, never the content. Each member fetches and unwraps only their own copy.
Sending costs one seal and one wrap per member, so a ten-member room means ten encrypted copies. Receiving costs the same whatever the group size.
What it costs on a microcontroller#
Sending to a room costs one encrypted wrap per member, and on the badge the largest cost today is a slow base64 routine, not the cryptography.
Per member, the sender does 1 key generation, 2 ECDH agreements, 2 NIP-44 encryptions and 2 Schnorr signatures; the elliptic-curve work runs in C, the rest in MicroPython. Receiving is the same for any group size: 1 signature check, 2 ECDH agreements and 2 decryptions.
Measured on a laptop running the same MicroPython code (Apple M1 Max, two independent runs), building the wraps for a 100-character message took:
| Members | Today's base64 | With the C base64 |
|---|---|---|
| 1 | 3–5 ms | 1 ms |
| 3 | 19–26 ms | 3–4 ms |
| 5 | 45–65 ms | 6–7 ms |
| 10 | 281–399 ms | 16–17 ms |
At 280 characters and 10 members the current code takes 0.6–1.4 s, against 20 ms with the C base64. The cause is a pure-Python base64 in the firmware's compatibility layer that builds one unmasked big integer over the whole input, so its cost grows with the square of the size; it shadows the C implementation on the device as well. Removing that override is a one-file change and is under way.
On the ESP32-S3 itself nothing has been measured yet. Scaling the operation counts to that chip gives an estimate of 0.3–0.8 s per member after the base64 fix, dominated by the six elliptic-curve operations, and likely several seconds for a 5–10 member room with today's code. Treat those figures as an order of magnitude until a device run exists. A wrapped message is 1.7 KB for one member and 3.9 KB for ten members at 280 characters, well inside common relay limits.
Gaps found in the current implementation#
The existing NIP-17 code has only ever been tested against itself, and it misses several requirements that other clients rely on. Each applies to one-to-one messages as much as to rooms.
- The inner message has no id. NIP-17 says "Fields id and created_at are required"; nostr-tools sets it, and clients store messages by it.
- No copy for the sender. The spec wraps a message "to each receiver and the sender individually"; without that, a user's other devices never see messages sent from the badge.
- Timestamps are not randomised. The seal and wrap "SHOULD randomize created_at in up to two days in the past"; today all wraps of a message carry the same timestamp and are published together, so a relay can link them as one group message.
- The subject is never sent or updated, although the app stores it as the room's title.
- Wraps go to the sender's own relays. NIP-17 says clients "MUST only publish events to the relays listed in the recipient's kind 10050 event"; delivery works today only when both sides share a relay.
- No NIP-42 AUTH. NIP-17 recommends that relays serve gift wraps only to the addressed user, enforced with AUTH; the badge drops AUTH frames, so it cannot read such relays.
- The user's own key is not removed from a typed member list, which would create a room id that replies never match, and the per-member encryption loop never yields, so a large send freezes the screen.
What happens next#
A "New group" mode and the library fixes are being implemented now, as two pull requests: one on the MicroPythonOS app and one on its micropython-nostr library.
- App: a Group mode with a member list and a subject, the user's own key stripped and the list capped at 10, the subject sent with every message and applied when it changes, the encryption loop yielding to the screen with progress, and the sender's own copy deduplicated against the local echo.
- Library: the inner message gets its id, seal and wrap timestamps are randomised independently, and a wrap is added for the sender.
- Deferred: publishing to each member's inbox relays (kind 10050), NIP-42 AUTH, and per-wrap delivery tracking.
- Before release: a real exchange with 0xchat or Amethyst, and timings on the ESP32-S3.
Longer term the ecosystem is moving to Marmot. Running it on this hardware would need an MLS implementation that fits a microcontroller and X25519, Ed25519, AES-GCM and HPKE in the firmware; none of these exist today, so the plan is to track the protocol rather than build it.
Sources#
- NIP-17: Private Direct Messages, NIP-59: Gift Wrap, NIP-44: Encrypted Payloads
- NIP-29: Relay-based Groups, NIP-42: Authentication of clients to relays
- Marmot protocol: MLS requirements and Nostr transport; Marmot Development Kit
- Client support: nostrability DM compatibility matrix, 0xchat README, Damus NIP-17 draft PR
- Relay side: relay29 (archived), groups.nip29.com
- MLS libraries: OpenMLS, mls-rs, mlspp, PyMLS
- Code: MicroPythonOS (Nostr Chat app under
internal_filesystem/apps/com_micropythonos_nostr), micropython-nostr (nostr/nip17.py,nostr/nip44.py) - Timings: measured on 15 October 2026 with the MicroPythonOS desktop build of MicroPython 1.27; ESP32-S3 figures are estimates

Comments
No comments yet. Start the conversation.