Nostr Compass #30
Welcome back to Nostr Compass, your weekly guide to Nostr.
This week: the Marmot spec is marked adopted across 42 files as MDK cuts v0.9.0 through v0.9.3 with encrypted group avatars, external signer support, and MarmotKit iOS and Android bindings. Mostro ships Transport v2 on NIP-44 direct messages with anti-spam gates and a coexistence window in both mostrod v0.18.0 and Mobile v1.3.0. Bitchat 1.6.0 adds NIP-13 proof-of-work to geohash channel messages, an opt-in mesh-to-Nostr gateway that lets one online phone uplink a whole crowd, prekey bundles, transitive verification, and creator-managed encrypted private groups. Amber scopes profile subscriptions per account, fetches NIP-65 relay lists before profile metadata, and adds a live Tor status notification with a restart action. rust-nostr adds NIP-40 expiration to gift wrap and NIP-17 DM builders, anchored to the wrap’s randomized timestamp. Amethyst merges 43 PRs of negentropy sync hardening, NIP-50 full-text search infrastructure, and event kinds for niche verticals. Nostrord ships v2.0.0 and v2.1.0 with a folded relay pool, zombie WebSocket detection, and a full disk-first cache seam. Ngit v2.6.2, Jumble v26.7.1, Applesauce signers 6.2.2, Bray v1.33.0, Deepmarks 1.0.0, Bitcredit Core v0.5.13, Coop Mobile v0.2.4, Granary v11.0, Nostr-relay v0.0.244, Manent v1.4.0, Routstrd v0.3.7, Nymchat 1.0.1, and 21Meetup 1.1.0 also ship, and SafeBox marks Phase 3 substantially complete alongside a FreeBSD jail deployment runbook and an OpenETR spin-off for electronic transferable records. The NIPs repository merges a NIP-51 and NIP-37 name alignment and opens five proposals: NIP-AD Nostr Web Addresses, NIP-86 invite-code claim management, an HSL role color format, NIP-80 hardware-attested media provenance, and a pagination fix in NIP-01. Deep dives cover NIP-13 (proof-of-work) and NIP-40 (expiration timestamp).
Lead stories
Marmot marks the spec adopted and MDK cuts v0.9.x
The Marmot protocol repository merged PR #170 on July 3, changing 42 files from Status: draft for internal review (and experimental draft) to Status: adopted. The README title moved from framing the repo as a work in progress to “Marmot Protocol” as the adopted text, the MIP-era documents were re-framed as the deprecated version of the protocol, and the “Review Status” section (“This is not adopted spec text yet”) became “Review Guidance” for editing the current spec. The v2 label disappears throughout: MIP-contrast phrasing (“new in v2”, “the v2 spec keeps”) is replaced with “this spec” and “under this spec”. Two documents keep their draft status by design: implementation-model.md remains non-normative, and the multi-device feature’s own document stays a draft.
The same repository landed PR #171 aligning admin-policy, membership, and role-change invariants. The cross-component check that a Remove cannot orphan an admin is now stated as a property of every resulting epoch, evaluated against the prior epoch’s admin set when a commit does not carry an admin-policy update. Convergence’s candidate-branch rule is tightened so “validates” means full commit validity including cross-component resulting-epoch checks, which prevents an invariant-violating commit from creating a candidate edge on any branch. State notifications derived from a superseded commit MUST be withdrawn when branch selection replaces it, which closes the “losing rename renders as a successful system message” bug at the spec level. A new “Realizing removal” section in member-departure.md defines the primary realization input (the accepted canonical commit removing your last leaf) and the fallback for clients that never applied the removing commit: authenticated post-eviction evidence now surfaces as a SelfEvicted outcome with retain-inactive semantics for the removed group copy. PR #236 then tightened wire-boundary validation, pinning KeyPackage lifetime acceptance to 84 days plus a one-hour skew margin, adding a Nostr tag-cardinality table for group h, gift-wrap p, welcome e and relays, and KeyPackage tags, and stating that unverified Nostr event ids and metadata are not trusted routing, replay, or telemetry evidence.
Downstream, the MDK workspace cut v0.9.0 on July 6 with a full workspace version bump, followed by v0.9.1, v0.9.2, and v0.9.3 over the following two days. v0.9.0 rotates stale keyring entries when a new SQLite database is created and lands validate-before-mutate discipline across the storage layer. v0.9.1 routes every outbound connection through one host-safety dial chokepoint via PR #732, closing the class of bugs where different call sites reached the network with different validation. v0.9.3 exposes encrypted group avatars to the uniffi bindings through download_group_image and image_hash_hex via PR #771, adds external-signer support, and marks wn-opencode production-ready via PR #781. Alongside the MDK cuts, MarmotKit ships iOS and Android bindings at each version (a MarmotKit.xcframework plus Swift bindings for iOS and Kotlin bindings plus JNI libraries for Android, both generated from a pinned MDK commit hash), and a new wn-agent release channel provides shell installers that pin the WN Agent version to an immutable release tag so downstream apps can pull the current agent with a single curl command.
Mostro v0.18.0 and Mobile v1.3.0 ship Transport v2 on NIP-44
Mostro is the peer-to-peer Bitcoin trading protocol that runs order books, escrow, and dispute resolution over Nostr events, coordinated by a daemon (mostrod) that clients speak to over encrypted DMs. Until this week the wire protocol between clients and mostrod was Transport v1. Mostro v0.18.0 lands Transport v2, wiring the protocol onto NIP-44 direct messages with anti-spam gates and dual-receive support running server-side. PR #776 is the Phase 1 wire change, PR #780 adds the Phase 2 anti-spam gates for protocol v2, and PR #785 makes the inner protocol version follow the active transport so a v2 client and a v1 client can coexist during the migration window. A related PR #782 fixes a NIP-33 info tag by renaming protocol_versions to the singular protocol_version. Alongside the transport work, the release lands a Phase 4 unified live-quote path with cache-and-staleness enforcement (PR #783) and an El Toque fiat-cross provider covering the Cuban CUP and MLC pairs (PR #778). PR #779 adds a slashed-party notification on dispute slash so a user who lost their bond hears from the daemon directly; the previous behavior surfaced only as a missing wallet balance.
Mostro Mobile v1.3.0 is the client half of the migration. PR #613 migrates the app to Riverpod 3.x, Phase A (PR #620) adds dual-receive support for NIP-44 direct messages on the main isolate and in the background isolate so a v2 mostrod and a v1 client can talk during the migration, Phase B in PR #624 adds dual-send, PR #632 re-applies dual-send after the Riverpod 3.x cut, and Phase C in PR #637 finalizes the migration. The release also adds African payment method coverage: PR #625 adds Malawi Kwacha payment methods and PR #627 adds KES (Kenyan Shilling), MZN (Mozambican Metical), TZS (Tanzanian Shilling), UGX (Ugandan Shilling), ZAR (South African Rand), and ZMW (Zambian Kwacha) methods while expanding NGN (Nigerian Naira). A restore flow now waits for node connectivity before issuing restore requests, and cause-aware handling distinguishes a dispute-driven bond slash from a timeout-driven one.
Bitchat 1.6.0 adds NIP-13 proof-of-work and an opt-in mesh-to-Nostr gateway
Bitchat 1.6.0 is the Bluetooth-mesh chat app that uses Nostr for its geohash channels and DM handoff. The release does two Nostr-shaped things worth reading. PR #1382 adds NIP-13 (proof-of-work) to outbound geohash channel messages (kind 20000 ephemeral events): each send mines a ["nonce", "<value>", "<target>"] tag before publishing, targeting 8 leading zero bits, which averages 256 hash attempts and completes in under one millisecond on an M-series Mac. Inbound events with validated PoW relax the per-sender intake rate limit, so a spammer pays compute per message while a regular sender does not feel the cost. Scope is deliberately narrow: only kind 20000 channel messages mine PoW, and presence heartbeats (kind 20001), kind-1 location notes, and DMs are untouched.
PR #1384 adds gateway mode, an opt-in mesh-to-Nostr uplink for geohash channels. When a mesh-only user (no internet, no reachable relay) sends in a geohash channel and another peer on the mesh advertises the .gateway capability, the signed kind 20000 event is wrapped in a new MessageType.nostrCarrier = 0x28 TLV envelope and sent directed to one gateway. The gateway peer publishes the event to Nostr on the sender’s behalf and rebroadcasts inbound channel traffic back onto the mesh with default TTL. Uplink deposits ride the courier envelope path (directed, relayed multi-hop); downlink rides broadcast. The signature happens before the event leaves the sender, so the gateway can decide whether to publish but cannot forge attribution. The stated motivation is disaster and protest scenarios where one connected phone in a crowd is enough to give the whole geohash channel a working Nostr uplink.
The same release ships a second batch of Nostr-adjacent work. PR #1381 adds prekey bundles for forward-secret asynchronous first contact on the courier mail path, so a sender can compose a message to a peer who is offline and hand it to the mesh without having done a live Noise handshake first. PR #1380 adds transitive verification: a peer that has completed the Noise handshake with someone you have already verified is now vouched for over the Noise session, so the trust graph propagates one hop at a time instead of requiring a fresh in-person verification for every new contact. PR #1383 adds creator-managed encrypted private groups over the mesh, PR #1376 detects, renders, and redeems Cashu ecash tokens with a /pay command, and PR #1379 adds a persistent signed geohash bulletin board layered on mesh sync. PR #1372 expands store-and-forward with open couriers, spray-and-wait routing, a persistent outbox, and a six-hour public history window. Bitchat 1.5.4 shipped earlier in the week with the end-to-end favorites fix in PR #1367 that cleans up peer-list duplicates, Nostr sync, and /fav key corruption.
Tagged releases
Amber v6.2.3 scopes profile subscriptions and adds a Tor status notification
Amber v6.2.3 is a performance and correctness pass on the Android NIP-46 signer, and the merged PRs in the week around it point at a coherent theme. The release itself adds a configurable profile fetch interval setting with never and always options (PR #492), shows a profile picture in the account switch bottom sheet, and scopes profile subscriptions by the current account so a signer holding multiple accounts stops fanning out subscriptions for accounts the user is not currently signing with. Bunker permission parsing gains explicit error handling on parse failures. Several StrictMode violations are fixed: a DiskReadViolation from Coil’s onSuccess logging, a keystore violation from loading the account on the main thread, main-thread reads for the account name and picture in the account switch sheet, and eager KeyPair() construction on the login and signup screens now moved off the main thread. In the days after v6.2.3 shipped, PR #493 reordered the boot path to fetch the user’s NIP-65 relay list before profile metadata (so the profile fetch queries the relays the user publishes to), and PR #494 turned the built-in Tor notification into a live status indicator with a restart action, so a user whose Tor daemon dies during a signing session sees the failure and can bounce it without leaving the signer. PR #495 enabled Android Lint in strict warnings-as-errors mode across the codebase.
Jumble v26.7.1 makes Blossom the default upload service in a DM-focused cut
Jumble v26.7.1 is a Nostr web client cut focused on direct messages and media. The release redesigns media upload settings and makes Blossom the default upload service, replacing the previous NIP-96 default. DM handling gets a mobile message menu, improved desktop message actions, a “scroll to latest” button, long-press reactions on DM media, and a retry path for failed outgoing DMs from the message list. Custom emoji editing gains a detail view, message bubble sizing improves for invoices and embedded content, several DM scrolling and message ordering issues are fixed, and post-editor issues around emoji insertion, text copy, and file drag are cleaned up. Image orientation is corrected when metadata is stripped on upload, and Linux ARM64 downloads are added to the release matrix.
Applesauce signers 6.2.2 drops an nbunksec dependency
applesauce-signers@6.2.2 drops the sub-package’s @sandwichfarm/encoded-entities dependency in favor of a built-in nbunksec helper via commit d654349. Applesauce’s NIP-46 bunker session encoding, added last week, no longer requires the external encoding library, cutting one supply-chain surface for downstream clients that consume the signers package.
Ngit v2.6.2 stops duplicate PR status events on default-branch push
Ngit v2.6.2 is a bug-fix release for the git-over-Nostr CLI. git push to the default branch stops publishing duplicate PR merge/applied status events for PRs that are already marked applied, because merge detection now reads the pre-push Nostr repo state (the source of truth for whether a PR was already resolved on the NIP-34 side of the workflow); the previous heuristic relied on git internals and duplicated the status event. Active repositories using ngit for git-over-Nostr push flows stop emitting duplicate kind-1621 status events into their audience.
Bray v1.33.0 CLI picks up a bunker profile, persona, and Tor outbound
Bray v1.33.0 is a Nostr SDK-plus-CLI release. bunker --profile <name> gets an auto-stable connection key and relay fallback so a saved profile can survive a relay outage; bunker --persona <name> signs as a derived nsec-tree identity, letting one signer act as multiple pubkeys from a single derived tree; and all HTTP fetches can be routed through a Tor SOCKS proxy when configured. The release adds wallet subcommands for NIP-47 NWC, NIP-29 group admin write operations (create, update, add-user, remove-user, set-roles), NIP-86 admin verbs, and NIP-65 outbox helpers. Publishing verbs pick up --jsonl, --csv, and --tsv output flags, a req verb for generic NIP-01 filter queries, an event verb for arbitrary event construction, a publish-raw command that signs and broadcasts pre-built events, a bunker sign one-shot NIP-46 signing command, and a per-command --relay flag on every publishing command. Security work covers three batches of audit deferrals: secret zeroisation discipline, HTTP transport bearer-auth and rate-limit hardening, and SSRF validation on relay URLs. The npm tarball ships at 533,844 bytes with a byte-identical reproducible build verified across two independent CI runners.
Deepmarks 1.0.0 hardens the Nostr bookmarking surface
Deepmarks 1.0.0 is a security-hardening 1.0 milestone for a public Nostr bookmarking service. Every bookmark is still a signed Nostr event that any client can read. The API and archive worker sit in a privileged network position (they can reach internal Redis, the bunker’s relay path, and cloud metadata), so the SSRF guard is load-bearing, and the release fixes a critical IPv6-literal bypass in isPrivateIp: bracketed IPv6 literals were being classified as public, so [::1], [fd00::1], and IPv4-mapped [::ffff:10.0.0.4] all reached internal targets over dual-stack connect. The guard now strips brackets and folds IPv4-mapped and IPv4-compatible IPv6 down to the embedded v4 before the private-range check on both boxes. Ingested kind:0 profiles from external relays are now signature-verified at the sink so a hostile relay cannot forge a nip05 or lud16 for an arbitrary victim pubkey, and bookmark URLs are scheme-checked at every render sink so a kind:39701 bookmark published straight to the relay with a javascript: or data: d-tag stops reaching an <a href>. Zap receipts now survive a transient bunker outage: the settlement handler atomically claims the pending zap, finalizes only after signing succeeds, and releases the claim on failure so a redelivered invoice_updated can retry. The /publish fan-out drain uses BLMOVE into a per-worker processing list with heartbeat-gated recovery so a crashed worker preserves a signed event the client was already 202’d for.
Bitcredit Core v0.5.13 unencrypts block metadata on the Nostr wire
Bitcredit Core v0.5.13 removes an encryption layer from the Nostr public events used by the credit-bill protocol. Block metadata (block id, hash, signature) is now unencrypted on the Nostr wire; only the block data itself remains encrypted with the corresponding bill key. New apps process old chains, old apps do not process new chains. The release also adds a bill-service function to fetch the bill chain, and switches publishing to an optimistic threshold model: once a configured relay threshold (default one) accepts a publish, remaining relays receive the event asynchronously so publishing is no longer blocked by the slowest relay.
Coop Mobile v0.2.3 and v0.2.4
Coop Mobile shipped v0.2.3 on July 4 and v0.2.4 on July 7, continuing the Android NIP-17 direct-messaging client’s steady release cadence. v0.2.3 adds inline image and link rendering in chat messages, image attachments, speech-to-text input, and a confirmation dialog for contact removal. v0.2.4 fixes an indicator that got stuck forever, improves the Nostr Connect handshake, and adds ncryptsec1 import (the NIP-49 encrypted-private-key format) alongside a redesigned import identity screen.
Granary v11.0 adds NIP-71 video event support
Granary v11.0 is the multi-protocol conversion library that powers Bridgy Fed’s cross-network bridging. The Nostr module gets three visible changes. NIP-71 video events (kinds 21, 22, 34235, and 34236) now convert into ActivityStreams 1 notes with video attachments, and the converter extracts the imeta image (thumbnail), the video duration, the top-level published_at tag, and the alt tag as a fallback displayName on the first video or audio attachment. On the API side, sign is renamed to hash_and_sign and verify now raises ValueError on failure; the Nostr constructor raises ValueError on an invalid relay URL, and Nostr.query skips the NIP-42 AUTH challenge gracefully when the caller has not set a privkey. A follow-up conversion fix stops crashes when a Nostr article object arrives without an id. Any bridge or reader consuming NIP-71 video events through Granary can now surface them in the format the target reader expects.
Nostr-relay v0.0.244 adds a Firestore backend
mattn/nostr-relay v0.0.244 adds a Firestore backend via PR #12, extending the Go relay’s storage layer with a Google Cloud Firestore option alongside its existing backends. The change is small but opens Firestore as a managed serverless database option for a relay operator.
Manent v1.4.0 fixes NIP-42 AUTH and adds media clipboard flows
Manent v1.4.0 is the encrypted notes and file storage app built on Nostr with NIP-44 encryption, NIP-46 and NIP-55 signer support, NIP-65 outbox routing, and Blossom storage. The release fixes NIP-42 relay authentication (previously broken), corrects Blossom uploads to http:// hosts (previously mishandled), and rewrites the compression flow. On the media side, users can now copy an image to the clipboard, paste an image from the clipboard, drag and drop files, crop and rotate images, play video and gifs, and take a video with a long press on the camera icon. On Linux, the primary clipboard is accessible via mouse middle-click. Note-loading and scrolling receive several optimizations.
Routstrd v0.3.7 makes the Nostr event store the persistent source of truth
Routstrd v0.3.7 is the local daemon for the Routstr decentralized AI inference network, which routes LLM requests via Nostr kind 38421 provider discovery and kind 38425 LGTM reviews. The release adds a routstrd update subcommand that downloads new binaries for both routstrd and cocod and gracefully restarts running daemons; the daemon now calls refreshNostrEvents() on startup and every 21 minutes so provider discovery and reviews stay fresh without manual intervention. The bundled @routstr/sdk upgrades from 0.3.12 to 0.3.15, removing the ProviderRegistry layer in favor of direct DiscoveryAdapter use, cleaning up models from disappeared Nostr providers so they no longer leak into rankings, and treating the Nostr event store as a persistent source of truth (the erroneous 210-minute TTL on cached events is gone). Xcashu refund handling tightens: refund tokens are tried before originals in the error path, 404s retry 3× with two-minute intervals, and 425 Too Early is handled without throwing.
Nymchat 1.0.1 launches as a Progressive Web App on NIP-17
Nymchat 1.0.1 (also known as NYM, Nostr Ynstant Messenger) is a Progressive Web App and native iOS/Android messenger for ephemeral chat over Nostr, bridged with Bitchat. Channels use kind 20000 ephemeral events for geohash channels and kind 23333 for named channels; private messages and group chats ride NIP-17 gift-wrapped events (kind 1059) with rotating ephemeral recipient keys and automatic post-compromise recovery. Users can generate a per-session ephemeral keypair with no registration or log in with a persistent identity via NIP-07 browser extensions, a NIP-46 remote signer, or an nsec. Optional device-local identity encryption uses password, PIN, passkey, or biometric unlock via WebAuthn PRF (passkey and biometric) or PBKDF2 (password and PIN), with the plaintext key never written to disk while encryption is on. Voice and video calls use NIP-17 gift wraps for signaling and WebRTC for the media path. Message reactions use NIP-25, custom emoji use NIP-30, and the web app is served as static files plus Cloudflare Pages Functions acting as a privacy proxy for relays and media.
21Meetup 1.1.0 launches Nostr-signed attendance badges
21Meetup 1.1.0 is a Flutter app for the German Einundzwanzig Bitcoin community that records meetup attendance via NFC tags and rolling QR codes. Each attendance badge is a Nostr event (kind 21000) signed by the meetup organizer using BIP-340 Schnorr, so a participant accumulates a set of signed events attesting to specific meetups at specific block heights. The rolling QR code rotates every 10 seconds, so a badge cannot be minted remotely, and the NFC tag is only readable in physical proximity. A trust score is computed locally from the collected badges; the score can be presented as a QR code for verification during peer-to-peer trades. The app targets Bitcoin community reputation, not general-purpose Nostr social, but the badge events themselves are ordinary Nostr events any reader can verify.
Nostrord v2.0.0 and v2.1.0 fold the relay pool and heal zombie WebSockets
Nostrord v2.0.0 is a major cut of the KMP/WASM Nostr client that speaks NIP-29, NIP-42, NIP-44, NIP-46, NIP-57, NIP-65, and NIP-98. v2.0.1 shipped one day later via PR #166 with a release-blocking desktop fix: the packaged 2.0.0 (deb, rpm, msi, dmg) crashed at startup with NoClassDefFoundError: java/sql/DriverManager because the jpackage jlink image was missing the java.sql module the SQLDelight sqlite driver depends on; the fix adds java.sql to the runtime image, and the same PR routes optimistic send through the network layer so the message reaches the relay (the previous code path cached silently and never delivered), plus keyboard and scroll behavior on mobile web.
v2.1.0 followed on July 7 with the “relay pool fold” (PR #176), which unifies the previously separate NIP-29 focused relay socket into the shared pool. One reconnect scheduler now covers all relays, NIP-42 AUTH signing is bounded with retry, publishes fail closed and retry on auth-required, request-storm races in requestPrivateGroupData and fetchGroupPreviews are closed, kind-10009 user-group-list fetches batch per relay, and the mux_chat live subscription now covers every joined group (not just the opened one) and self-heals when a relay silently drops the subscription. UI-side changes replace the layout-shifting “Sending…” row with an inline clock-then-check icon and turn stalled scroll-back into an explicit Retry row. PR #179 landed the same day to detect zombie WebSockets on Android: mobile networks and Doze mode kill TCP without a close frame, so writes into the dead socket buffer locally without throwing and isConnected() stays true even though nothing will ever be received. NostrGroupClient now stamps lastInboundAtMs on every frame, gains markDead() (which cancels the frame loop so the normal reconnect and resubscribe path runs), and probeLiveness() (a REQ any relay must answer within 5 seconds), triggered on OK timeout with zero inbound frames or on mux stale plus socket frame silence. A second bug fix in the same PR stops optimistic messages being written to the persistent cache at insert time; they now write only after delivery confirmation. v2.1.1 shipped one day later via PR #178 adding iOS platform actuals, native test support, and app icons alongside the v2.1.0 zombie-WebSocket work.
Unreleased changes
rust-nostr adds NIP-40 expiration to gift wrap and private DM builders
rust-nostr merged PR #1384 adding an expiration option to GiftWrapBuilder and PrivateDirectMessageBuilder. The library takes a Duration from the caller: the NIP-40 expiration tag is anchored to the gift wrap’s randomized created_at (created_at + duration), which decouples it from the real send time. Letting a caller pass an absolute timestamp would leak the send time to a relay observer (subtract the duration and you recover the original send time), so the library builds the tag internally from the randomized wrap timestamp. The expiration tag goes on the gift wrap event, not on the kind:13 seal (which NIP-59 requires to have empty tags). NIP-17 hands the same value down to the gift wrap builder from PrivateDirectMessageBuilder. The change closes issue #1381 and lands via the same builder pattern rust-nostr uses for extra_tags. rust-nostr also merged PR #1387 consolidating nostr-relay-builder into nostr-sdk, a workspace-flattening move.
Amethyst spends the week hardening negentropy sync and adding NIP-50 search
Amethyst’s main branch merged 43 PRs across three coherent themes. The largest thread is negentropy sync on the geode-to-strfry boundary: a refused-window failure mode that used to storm the client into a window-split loop now backs off cleanly (PR #3480), the underlying negentropyKmp dependency moves to v1.1.1 (PR #3475), a 1-million-event geode-to-strfry benchmark lands with a strfry-parity mirror (PR #3478), and production benchmarks join the CI matrix alongside broader sync optimizations (PR #3458, PR #3466). Lock-free concurrent collections replace the previous mutex-per-relay pattern and a UDP socket threading fix rides along (PR #3459).
The second thread is NIP-50 full-text search infrastructure. A SearchableEvent interface lands so events can carry index metadata directly (PR #3452), and NIP-50 search extensions are now stripped before querying SQLite FTS so the local search engine no longer chokes on server-side extension syntax (PR #3464). Default search relays get centralized (PR #3446).
The third thread is protocol integrations for niche verticals. Support for Birdstar bird-detection events (kind 2473) reaches an Android client (PR #3473), and PS1 memory-card save states can be published as signed events on kind 38192 (PR #3482). Rounding out the week: a compose-signature setting auto-appends custom text to posts (PR #3450), the desktop notifications view is redesigned with native OS toasts and a shared filter (PR #3457), the Messages column picks up a privacy lock (PR #3432), NostrServer.ingest adds a local write path with per-submission verify skip (PR #3469), and equals/hashCode contracts are repaired in the OpenTimestamps verify path (PR #3477).
Buzz keeps hardening the relay and defines kind 44200 for agent turn metrics
Buzz (the project formerly named Sprout) landed 123 PRs merged in the July 1 through July 7 window. Two threads carry most of the weight. The first is a new event kind for agent telemetry: PR #1441 defines NIP-AM durable encrypted agent turn metrics as kind 44200, which lands the telemetry as a signed event the user’s own relay archives, keeping metrics on user-owned infrastructure. A local archive for the kind follows (PR #1555), the remove-kind path is made atomic (PR #1562), and the model name is threaded through the emit path so downstream readers can distinguish which model produced which turn (PR #1564).
The second thread is relay performance. Post-commit dispatch is deferred and a verify clone is avoided (PR #1453), ingest and fan-out DB round trips are batched with measured p99 ack drops of 7 to 16 percent and p999 tail drops of 29 to 53 percent versus the prior tip (PR #1454), multi-filter query execution runs with bounded concurrency (PR #1457), and outbound WebSocket data frames batch on send (PR #1464). Alongside the perf work, a per-community workspace icon set that admins configure and the relay serves via NIP-11 extends NIP-11’s information document with a per-community customization surface (PR #1463), agent owners can delete their agent’s messages via relay kind:5 events plus matching desktop and mobile UX (PR #1519), OpenTelemetry tracing joins Prometheus metrics on the relay (PR #1398), and the git repo-name registry moves to Postgres (PR #1432).
Divine Video wires up relay signature verification and a NostrConnect extraction
Divine Video’s mobile app merged 97 PRs in the window, and the Nostr-facing thread is trust boundary hardening plus authentication cleanup. PR #5774 verifies inbound relay event signatures, closing a class of trust-in-the-relay bugs; PR #5828 encrypts the FCM push token in the kind-3080 deregistration event so the user’s device token stops appearing in cleartext on the relay when they unsubscribe; and PR #5831 chunks the kind:5 deletion REQ so a user with a large delete history no longer overflows the relay frame. On the authentication side, PR #5826 extracts a NostrConnectCoordinator for the nostrconnect:// flow, cleaning up the NIP-46 client-initiated bunker code path ahead of a broader auth refactor tracked under issue #4741. PR #5709 maps kind-16 reposts when notification_type is absent so a repost notification renders correctly even when the sending client omits the hint.
Zap Cooking fixes NIP-46 bunker login and adds NIP-50 recipe search
Zap Cooking’s frontend merged 18 PRs in the window along one theme: making Nostr auth surfaces recover from failure. PR #503 fixes bunker login with an explicit connect handshake, authUrl handling, and error surfacing so a user attaching an external signer sees a real error message on failure where the previous cut hung the login screen. PR #495 adds NIP-98 auth to the extract-recipe endpoint’s image and text upload paths so uploads are pubkey-attributed. A separate feature thread lands NIP-50 full-text recipe search via the nostrarchives search relay backend (PR #483), letting a user query recipes across the relay corpus without a client-side index. Content-rendering polish ships alongside: quoted-note content and media now surface directly in the parent note replacing the previous buried-link fallback (PR #491), link previews and hashtag sizing land (PR #492), multi-word search queries work (PR #482), and server-side social preview cards are generated for note, reads, and profile links (PR #494).
swift-nostr-client v0.6.0 progresses toward a first stable cut
yysskk/swift-nostr-client shipped v0.6.0 alongside 30 merged PRs. The Swift Nostr library moves closer to a first stable API surface for Swift Nostr clients that avoid linking the MDK or MarmotKit toolchains.
Nostr Applet Protocol (NAPS) tightens NAP-OUTBOX Routing and Fanout
NAPS had a meaningful cleanup week, mostly in NAP-OUTBOX. The headline is tighter boundaries: less caller-controlled routing, fewer leaked relay details, and a shared event result shape that can carry relay hints and resource sidecars, tying into NAP-RESOURCE. Publishing is clearer too: explicit outbox, inbox, and relay fanout rules. Net effect: less ambiguity, better interoperability.
Napplet Toolchain Tightens Protocol Alignment and Ships Its CLI
This week, Napplet’s packages moved from “useful SDK” toward a tighter protocol toolchain. The big story is alignment with the live NAP specs: NAP-COUNT query support, OUTBOX’s runtime-owned lifecycle, and RelayEventResult sidecars all landed, making shell-mediated reads and subscriptions more precise. Several domains were sharpened too: CVM registry support, DM error envelopes, MEDIA session context, LISTS count fields, COMMON profile results, and the htree: RESOURCE scheme. On tooling, the new @napplet/cli is a major milestone, adding config discovery, deploy planning, signing, Blossom uploads, and manifest generation. Finally, the host-injectable shim prelude and JSR readiness work made the stack easier to inject, publish, and verify.
primal-android extends the remote-signer surface
Primal Android merged 18 PRs in the window. On the Nostr side, PR #1075 implements switch_relays and logout methods for the app’s remote-signer role, extending Primal’s NIP-46 signer surface. PR #1083 adds a splash-gated local app-migration framework, and PR #1080 implements note-feed prefetching in the splash view-model. The rest is UI polish across the Home top and bottom bar, Explore hints, and the profile screen.
Wisp adds a multi-account switcher and Blossom parser tests
Wisp merged 9 PRs. PR #604 adds a multi-account switcher with an explicit cancel path on the add-account flow. PR #613 adds unit tests for Blossom.parseServerList, tightening the Blossom server-list parser. PR #574 rewrites the zap sheet for iOS layout with an instant-zap settings surface, PR #605 turns transaction history into a swipe-up bottom sheet, PR #611 parses hashtags with non-ASCII Unicode letters, PR #609 keeps the profile notes feed paginating and renders inline gallery media, and PR #603 preserves blank lines before inline profile and hashtag segments.
TAO and Wired raise the PoW signal to 21 bits and surface fresh-PoW roots
smolgrrr/TAO and smolgrrr/Wired (the same commit set landed in both repos) merged 13 PRs. PR #84 raises the default post-signal proof-of-work target to 21 leading zero bits, and PR #80 surfaces feed roots from fresh PoW activity so a client can rank the timeline by recent NIP-13 work; the previous ranking was raw event age. PR #75 restores a custom emoji picker and PR #65 adds first-frame video previews. This is the second Nostr client this week to lean on NIP-13 as a first-class filter for user-generated content, complementing Bitchat’s channel-scoped PoW.
keep-android polishes NIP-46 UX and lands a TOCTOU fix
privkeyio/keep-android shipped v1.1.5 alongside 13 merged PRs, then v1.1.6 on July 8 pinning the underlying keep core to v0.5.0. Keep is a mobile identity vault (covered in Issue #29 as CustID). v1.1.5 was UX polish on the NIP-46 challenge flow. v1.1.6 closes a check-then-set (TOCTOU) race in set_active_share from the underlying keep-mobile crate, surfaces the URL and method being authorized on the NIP-98 HTTP-auth approval prompt so a user can see what they are signing, and switches the RNG health check to fail closed (return an error) instead of panicking. An instrumented test covers the NIP-55 approval-flow kill switch. The v0.5.0 CLI features that came with the underlying release (threshold-OPRF unlock, software DKG, HD FROST wallets) are not surfaced in the Android app yet; v1.1.6 delivers the security fixes only.
Heartwood ships the relay-to-serial signing bridge
forgesworn/heartwood v0.7.0 lands the relay-to-serial signing bridge that was in flight last week, wiring the HSM-mode data plane for Bray’s serial-signer path. PR #11 is the bridge itself, PR #13 adds serial-frame coverage and fixes the device read_frame payload offset, and PR #14 extracts the serial frame codec into a shared heartwood-frame crate.
SafeBox publishes a Phase 3 progress report and a FreeBSD jail runbook
SafeBox is a private portable data vault on Nostr that combines NIP-47 Nostr Wallet Connect, nAuth, nembed, and relay-mediated record transfer over QR and NFC into one operator-deployable service. A July 2026 progress report published on July 6 marks Phase 3 as substantially complete: 49 commits landed since the April report, bringing the repository to 1,136 commits, and the four Phase 3 engineering commitments (harden Phase 2 experiments, support interoperable instances, prepare for scale, add commercial-product discipline) are largely delivered. The report frames the next step as a bounded pilot, and discloses that a telecommunications provider under NDA is exploring a health-records pilot on SafeBox.
The concrete Nostr-facing work landed earlier in Phase 3 and is summarized in the report: mutating NWC actions are now queued to avoid proof races, failed Lightning melts protect proofs before returning, long-lived NWC listeners now refresh proactively so a session survives past its idle threshold; the previous behavior was a silent stall, and LNURL callbacks use canonical origins with explicit JSON and CORS responses. QR and NFC record exchange gained a unified flow spec covering recipient-presented, sender-presented, and cross-device presentation modes with clearer KEM (Key Encapsulation Mechanism) handling and replay protection through the Open Quantum Safe library. The in-window commit is 6866dae, which adds a FreeBSD jail deployment and liboqs build runbook alongside a FreeBSD appliance specification, documenting ZFS snapshots, jail isolation, rc.d service management, host-level reverse proxy configuration, and rollback procedure for a SafeBox deployment on FreeBSD/ARM hardware.
The report also announces OpenETR as a distinct spin-off applying SafeBox’s cryptographic-control-plus-portable-records architecture to electronic transferable records: bills of lading, warehouse receipts, promissory notes, and certificates. OpenETR’s repo saw 7 commits on July 7 including ea612a9 separating attestation from the core record, ca153a3 on mandate-versus-effect handling, and ba84b61 adding a comparison to verifiable-credential formats.
Protocol work and NIP updates
Merged: NIP-51 and NIP-37 align the kind 10013 name
PR #2404 is a prose-only consistency fix. In NIP-37, kind 10013 is named Relay List for Private Content; in NIP-51 under Draft relays, the same kind was described with different wording. NIP-51 now uses the NIP-37 name for the same event kind. No wire behavior changes and no new tag semantics; the value is that NIP-51 is the umbrella spec for list-shaped events and NIP-37 is the private-content follow-up, and misaligned naming between the two makes it easy to miss that they describe the same kind.
Open: NIP-AD Nostr Web Addresses via .well-known lookup
PR #2406 opens as the successor to a closed PR #2393 with a full spec draft at AD.md. NIP-AD defines web URLs that carry an optional Nostr counterpart. A client that sees a URL like https://golf.com/players requests https://golf.com/.well-known/nostr.json?ad=/players, which returns a JSON object mapping paths to {filter, relays} pairs. The returned filter is a standard NIP-01 filter (kinds, authors, #d, limit, etc.), and the relays array names which relays the client should query. With "limit": 1 the URL resolves to a single event; without it, to a list. In a normal web browser the URL renders HTML like any other URL, so the same domain can serve web users and Nostr clients from one canonical path. The stated use cases include NIP-29 group names resolving to a kind 39000 event on a specific relay (removing the need for group id farming), NIP-5A nsite lookups, hosted feeds that publish a {"ids": [...]} filter, native rendering of pasted njump.me/nevent1... and client-specific event URLs, and Nostr-fueled blogs that exist both natively inside Nostr and to visitors outside. The .well-known/nostr.json reuse plus path-as-object-key layout is chosen so the resolver can be a static file.
Open: NIP-86 claim management for invite codes
PR #2408 proposes adding three methods to NIP-86: listclaims (params [], returns an array of NIP-43 invite codes), createclaim (params [claim], returns true), and deleteclaim (params [claim], returns true). Today NIP-86 lets a relay admin manage users and role assignments but has no invite-code surface. The PR author’s use case is community-relay onboarding: an admin creates an invite code associated with a role, collects payment before the user’s identity is created, hands the invite code to the user, and a bot listens for the resulting kind 28935 claim event on the relay and auto-assigns the role. The three methods let that flow run entirely through the relay management RPC.
Open: role color as (h, s, l) tuple
PR #2402 changes the role color format in NIP-43 from a single hue value (0 to 360) to a tuple of hue (0 to 360), saturation (0 to 1), and lightness (0 to 1). Empty strings are permitted for any component so clients can supply their own defaults for a coherent palette, and the spec text recommends providing only hue unless a specific color like silver is desired. The change threads through NIP-86 in the same PR: createrole and editrole now take [id, label, description, [h, s, l], order]; the previous signature carried a single-color parameter in the same slot. The motivation is that hue alone forces clients to pick saturation and lightness for the operator, so different clients render the same role at visibly different intensities.
Open: NIP-80 hardware-attested media provenance
PR #2409 opens NIP-80, an event format for media provenance anchored in capture hardware. A camera signs each photo at the moment of capture and publishes the proof to relays keyed by the content itself, so verification survives metadata stripping, re-hosting, and platform takedowns. The proposal defines six new event kinds: kind 1080 for capture attestations, kind 1081 for derivation attestations covering resize, crop, recompress, or redact operations (with a reveal mode or zero-knowledge option), kind 1082 for revocations (regular events, permanent, author-scoped, monotonic), kind 11080 for device announcements, kind 31080 for device endorsements, and kind 31081 for a device set for anonymous attestations (marked experimental and possibly split into a companion NIP). Reused primitives include NIP-94 x-tag semantics, NIP-92 imeta, NIP-65 for revocation discovery, Blossom for media storage, and optional NIP-03 timestamp anchoring. The signing model pairs a BIP-340 device key with a hardware ECDSA key because mainstream secure elements do not yet produce BIP-340 signatures (Microchip ATECC608 supports P-256, NXP SE050 supports secp256k1 but only ECDSA, TPM 2.0 modules and Infineon OPTIGA Trust M cover P-256/RSA, Apple Secure Enclave and Android StrongBox use P-256). The stated scope explicitly does not attempt to prove the scene is real: an attestation proves this exact image came from this device at approximately this time and was modified only in declared, provable ways, and the specification forbids clients from collapsing results into a bare “authentic” badge. A working prototype OpenVeilCam, a Rust camera runtime for Raspberry Pi using the ATECC608 secure element, is being updated to publish the proposed event kinds alongside a standalone verifier.
Open: NIP-01 pagination hardening
PR #2407 adds a “Pagination & limits” subsection to NIP-01. The concrete rules: a relay that imposes a maximum limit MUST set it greater than the largest number of events sharing a single created_at in its database, so no single second can fill a page and stall pagination. Clients paging backwards MUST repeat requests with until = oldest (inclusive) and MUST deduplicate by id (since the oldest second is re-fetched each round), and paging is complete when a round yields no new events after deduplication. If a full page has oldest and newest events sharing one created_at, the client MUST retry that second with a larger limit, and if the relay clamps the larger limit and still returns a page confined to one second, the client MUST either advance with until = oldest - 1 (treating unretrieved events as dropped) or abort. Normal paging MUST NOT set limit; the relay maximum is authoritative, and a smaller value reintroduces the stall. Raising limit to drain a stuck second is the one exception. This fix matters because a naive since/until cursor either misses events with duplicate timestamps or reprocesses them, and the current NIP-01 text does not tell either side how to escape the trap.
NIP Deep Dive: NIP-13 (Proof of Work)
NIP-13 defines a proof-of-work mechanism for Nostr events. It exists because email-style spam is trivial to produce on a public relay network: anyone can generate a keypair and flood a topic, and there is no economic cost per event. NIP-13 lets an event author impose a computational cost per event that a spammer would have to pay in aggregate but a regular sender pays only once per message. Relays and clients can then require or prefer events that meet a difficulty threshold.
The mechanism
An event author picks a difficulty target expressed in bits and mines the event’s id (the sha256 hash of the serialized event) until it has at least that many leading zero bits. Because the event id includes the created_at timestamp, the tags, and the content, mining requires changing something in the event body to search the hash space. NIP-13 defines a nonce tag for exactly this purpose:
["nonce", "<nonce_value>", "<target_bits>"]
The nonce_value is any string the miner picks; the target_bits is the difficulty the miner committed to. A verifier counts the leading zero bits of the event id and compares against target_bits. The target_bits in the tag is a claim, and a verifier measures the actual leading-zero count of the id to confirm it.
The number of leading zero bits in a random sha256 output follows a geometric distribution: each additional bit doubles the expected work. 8 bits averages 256 hash attempts, 20 bits averages roughly one million, and 28 bits averages roughly 268 million. Bitchat’s 8-bit target for geohash-channel messages costs under one millisecond of CPU on modern hardware and completes below any perceptible latency. TAO and Wired’s 21-bit default is roughly two million hash attempts per post, which is fast on a laptop but expensive at scale for a bot farm. NIP-13 does not mandate a difficulty; each relay and client picks its own.
Example event
A minimal NIP-13-mined kind-1 note looks like:
{
"id": "000000000e9d97a1ab09fc381030b346cdd7a1a8a6f27c9c88f68c8b9d0f6c8a",
"pubkey": "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2",
"created_at": 1720368000,
"kind": 1,
"tags": [
["nonce", "72847", "28"]
],
"content": "hello, this cost me 28 bits of PoW",
"sig": "b1a5c9c74cff59f8a48e5c3b3d8e1c8e7e2c1d4a8e2b9f7d1c3e8b4f6a2c8d1e9f4b3c7a1d8e5b2f9c6a3d7e1b8f4c9a2d6e3b7f1c8a4d9e2b5f8c1a7d4e6b9f3c2"
}
The id starts with seven hex zeros (28 leading zero bits, matching the target_bits in the nonce tag). The miner varied the nonce_value 72847 until the id met the target. A verifier hashes the serialized event and confirms the id has at least 28 leading zero bits, then verifies the signature. NIP-13 adds no new fields; it adds the nonce tag and constrains the id’s zero-bit count.
Where it is used
Bitchat’s 1.5.4 release uses 8-bit PoW on kind 20000 geohash-channel messages: outbound sends mine the tag before publishing and inbound events with validated PoW relax the per-sender intake rate limit. TAO and Wired use 21-bit PoW as the default post-signal threshold and surface feed roots from fresh PoW activity, treating PoW as a timeline ranking signal. cagliostr enforces NIP-13 at the relay layer, rejecting events below a threshold. NoStrudel exposes a client-side PoW mining setting for authors who want to signal to filtering clients. Damus and Amethyst compute leading-zero bits when displaying events, letting a user see the PoW commitment on notes. Coracle exposes PoW both for mining and filtering. NDK and nostr-tools expose PoW mining helpers to library consumers.
The design property that shapes NIP-13’s deployment is that PoW is unforgeable: a claim of target_bits counts as evidence only when the id has that many leading zeros, and a counterfeit requires redoing the work. That property lets Bitchat use inbound PoW as a rate-limit relaxer even when a spammer claims a high difficulty; the check is a hash count, not a trust decision. The complementary property is that PoW does not commit the miner to any specific pubkey or content; a spammer can still choose to mine at 8 bits and burn compute, but the compute is a real cost. NIP-13 shifts the spam problem from “impossible” to “quantifiable” and lets clients set their own price.
NIP Deep Dive: NIP-40 (Expiration Timestamp)
NIP-40 defines an expiration tag that instructs a relay and a client that an event should be considered expired after a given Unix timestamp. It exists because Nostr events are otherwise permanent: once a signed event lands on a relay, the only way to remove it is a NIP-09 delete event, and even then a relay may retain the original. NIP-40 lets an author declare at publication time that an event is short-lived, and asks relays to stop serving it and clients to stop displaying it after the timestamp.
The mechanism
An author adds an expiration tag to an event:
["expiration", "<unix_timestamp>"]
The timestamp is Unix seconds. A relay MAY reject events whose expiration is already in the past at ingest, MAY stop serving events whose expiration has passed, and SHOULD respect the author’s stated expiration. A client SHOULD hide expired events from the user. NIP-40 does not require the relay to delete the event, and it does not overrule NIP-70 protected-event semantics; it is a hint plus a soft contract.
The tag lives on the event itself (or in the case of wrapped messaging, on the outer wrap). NIP-40 does not define delete semantics; the event remains a signed event that anyone who has it can still read. What NIP-40 gives is a coordinated expectation that the relay and the client will stop surfacing the event after the deadline. This makes NIP-40 useful for ephemeral posts, timed announcements, live-event notes that should stop being served after the event, and NIP-17 direct messages that should not linger past a stated horizon.
Interaction with gift wrap
The rust-nostr PR that landed this week (PR #1384) is a case study in how NIP-40 interacts with NIP-59 gift wrap. NIP-59 defines a two-layer envelope: a kind:13 “seal” event signed by the sender’s real key, and a kind:1059 “gift wrap” event signed by an ephemeral key. Both layers have randomized created_at values, up to 48 hours before the real send time, so a relay observer cannot recover the true send timestamp. NIP-59 mandates that the seal have empty tags.
That mandate is why the expiration tag has to go on the gift wrap and stay off the seal, and why anchoring the tag to the real send time would defeat gift wrap’s timing privacy: if a caller passes an absolute expiration timestamp, an observer subtracts the caller’s intended TTL and recovers the real send time. rust-nostr’s design decision is to expose the API as a Duration from the caller, then compute expiration = wrap.created_at + duration inside the library. The wrap’s created_at is already randomized inside the library, so the expiration timestamp inherits the same randomization and does not leak the true send time.
Example event
A minimal NIP-40 example on a kind-1 note:
{
"id": "1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
"pubkey": "82341f882b6eabcd2ba7f1ef90aad961cf074af15b9ef44a09f9d2a8fbfbe6a2",
"created_at": 1720368000,
"kind": 1,
"tags": [
["expiration", "1720454400"]
],
"content": "this note expires in 24 hours",
"sig": "d2e5b8a1c4f7b0d3e6a9c2f5b8d1e4a7c0f3b6d9e2a5c8f1b4d7e0a3c6f9b2d5e8a1c4f7b0d3e6a9c2f5b8d1e4a7c0f3b6d9e2a5c8f1b4d7e0a3c6f9b2d5e8a1"
}
created_at is the Unix timestamp of publication; the expiration tag says the event should stop being served 86,400 seconds (24 hours) later. A relay that respects NIP-40 stops returning this event to REQs after 1720454400, and a client that respects NIP-40 hides it from the user after that time.
Where it is used
rust-nostr’s builders (GiftWrapBuilder, PrivateDirectMessageBuilder) now expose expiration as a first-class Duration parameter. NDK exposes an expiration helper for kind-1 and DM builders. nostr-tools has a getExpiration and isExpired pair for reading and enforcing the tag. strfry, nostr-rs-relay, khatru, and other relay implementations respect NIP-40 in REQ handling (rejecting or omitting expired events depending on the operator’s policy). Damus, Amethyst, noStrudel, Coracle, and Primal all filter expired events from their timeline rendering. Live-activity clients like zap.stream use NIP-40 on the associated kind-1311 chat events so a live chat stops persisting after the stream ends.
The design property that lands NIP-40 cleanly in most implementations is that it is opt-in per event and does not require coordinated deployment. An author can add the tag today; a relay that honors it gets a cleaner working set; a relay that ignores it does no worse than before; and a client that hides expired events gives the author what they asked for. The rust-nostr change this week reinforces that the tag’s placement matters as much as its presence: in a privacy-preserving envelope like NIP-59 gift wrap, the tag sits on the layer whose timestamp is already randomized, and the API surface prevents a caller from accidentally leaking a real timestamp back into the wrap.
That’s it for this week. Building something or have news to share? Reach out via NIP-17 DM or find us on Nostr.