RoomKit 0.37.0 adds first-class support for Buzz, Block's Nostr-based team workspace where humans and AI agents are both cryptographically-identified members. A RoomKit agent can now chat in Buzz channels, announce its presence, and — when someone opens a voice huddle — join it and talk. It ships as pip install roomkit[buzz], powered by a new sibling package, buzzkit.
What Buzz Is
Buzz is a team workspace built on Nostr. A community is a closed relay: an identity must be a member of the relay before it can read or write anything — every request from an outsider is refused. Channels are NIP-29 groups, chat messages are Nostr kind 9 events, and identity is a keypair rather than an account.
That last part is what makes Buzz interesting for agents. There is no bot token to provision and no app review process. You generate a keypair, redeem an invite link, and the agent is a member — with the exact same identity machinery as a human: its events are signed with its key, it authenticates to the relay with NIP-42 over WebSocket and NIP-98 over HTTP, and it can carry an owner attestation (NIP-OA) that says which human is responsible for it.
One honest disclaimer up front: buzzkit is an unofficial client, not affiliated with Block.
buzzkit: Rust Crypto, Python I/O
buzzkit is the client library underneath RoomKit's Buzz support. Its design splits along a clean line:
- The cryptographic core — Schnorr signing, event building, verification, NIP-42/98 auth — is Rust, built with PyO3 on top of Block's own zero-I/O
buzz-coreandbuzz-sdkcrates. - All network I/O is pure Python (
httpxandwebsockets), so the async story stays idiomatic — no tokio ⇆ asyncio bridge.
It ships abi3 wheels for CPython 3.12+ on Linux, macOS, and Windows. Because it is a compiled wheel, the buzz extra is deliberately kept out of RoomKit's aggregate providers/all extras — you opt in explicitly with pip install roomkit[buzz], and RoomKit's own CI never has to build it.
The Text Channel
Buzz is a transport channel in RoomKit, like SMS or Discord — a new ChannelType.BUZZ with its own source, provider, and factory. The architecture mirrors the Discord integration: one buzzkit.BuzzClient, one Nostr identity, both directions.
BuzzRelaySourceowns the client. Inbound is a WebSocket subscription with NIP-42 auth, with automatic reconnect (1 s backoff doubling to 30 s).BuzzProviderborrows the same client for outbound. Sends go through the relay's HTTP bridge rather than the WebSocket — deliberately, so an outbound message succeeds even while the inbound socket is mid-reconnect.
Configuration is one small pydantic model:
class BuzzConfig(BaseModel):
relay_url: str
private_key: SecretStr # nsec... or hex; signs events, authenticates (NIP-42/98)
ignore_own: bool = True # echo guard: drop the agent's own events
auto_join: bool = True # NIP-29 kind 9000, role=bot
announce_presence: bool = True # kind 20001 "online", re-announced every 55 s (TTL 90 s)
auth_tag: str | None = None # NIP-OA owner attestation
The housekeeping a workspace member is expected to do is handled for you: the agent joins the channel as a bot on connect, heartbeats its presence well within the 90-second TTL, and filters out the echo of its own messages. Wiring it into RoomKit looks like every other channel:
from roomkit import BuzzChannel, RoomKit
from roomkit.providers.buzz import BuzzConfig, BuzzProvider
from roomkit.sources.buzz import BuzzRelaySource
config = BuzzConfig(
relay_url="wss://your-community.communities.buzz.xyz",
private_key="nsec1...", # the agent's Nostr secret
)
source = BuzzRelaySource(config, "buzz-main", relay_channel_id="<channel-uuid>")
provider = BuzzProvider(source) # reuses the source's client
kit = RoomKit()
kit.register_channel(BuzzChannel("buzz-main", provider=provider))
await kit.create_room(room_id="buzz-room")
await kit.attach_channel(
"buzz-room", "buzz-main", metadata={"buzz_channel_id": "<channel-uuid>"}
)
await kit.attach_source("buzz-main", source) # connects and subscribes
From here, everything RoomKit already does applies unchanged: attach an AIChannel to the room and the agent answers in the Buzz channel; add hooks and the guardrail pipeline runs on every inbound event; attach an SMS or WebSocket channel to the same room and Buzz becomes one leg of a multi-channel conversation.
Onboarding is a one-time step per agent key:
from buzzkit import BuzzClient, generate_keypair
nsec, npub, _ = generate_keypair() # your agent's identity: store nsec securely
client = BuzzClient("wss://your-community.communities.buzz.xyz", nsec)
await client.claim_invite("https://your-community.communities.buzz.xyz/invite/<code>")
Voice: Answering the Huddle
Buzz has huddles — ephemeral voice channels, announced on their parent text channel as a Nostr event (kind 48100). The audio protocol is fixed: Opus, 48 kHz mono, 20 ms frames, over the relay's WebSocket. RoomKit 0.37.0 turns that into a realtime voice transport, so a huddle can be answered by the same speech-to-speech stack that already drives SIP calls and browser sessions.
Two pieces do the work:
BuzzHuddleWatcher subscribes to huddle announcements on a channel and dials in when one opens. The announcement feed gets its own room, deliberately — it is an input, not a conversation participant — and a hook on that room triggers the bridge. It handles one call at a time; announcements arriving mid-call are ignored.
BuzzHuddleBackend is a standard RoomKit VoiceBackend, which means VAD, interruption handling, and provider session management all apply as usual. The details it takes care of are the ones you would otherwise learn the hard way:
- Resampling is the backend's job. The huddle protocol is fixed at 48 kHz; the backend resamples to and from the provider's rates itself using a streaming soxr resampler. This matters audibly: the generic fallback is linear interpolation, whose imaging artifacts on a 24 kHz → 48 kHz upsample make synthesized voices sound harsh and saturated. soxr keeps the spectrum clean.
- Silence fill. Buzz clients use DTX — they stop sending frames during silence. The backend drops comfort-noise frames and runs its own silence ticker, so the provider-facing stream stays continuous.
- Leaving when alone. The relay keeps a huddle alive while any member is connected — the agent included. Without
end_when_alone(default on, with a 90-second grace period), the huddle and the provider session would run forever after the last human left.
A complete voice agent, condensed from examples/buzz_voice_agent.py:
from roomkit import RealtimeVoiceChannel, RoomKit
from roomkit.providers.buzz import BuzzConfig
from roomkit.providers.gemini.realtime import GeminiLiveProvider
from roomkit.voice.backends.buzz_huddle import BuzzHuddleBackend, BuzzHuddleWatcher
kit = RoomKit()
voice = RealtimeVoiceChannel(
"buzz-voice",
provider=GeminiLiveProvider(api_key=..., model="gemini-3.1-flash-live-preview"),
transport=BuzzHuddleBackend(),
system_prompt="You are a helpful voice assistant.",
# No transport_sample_rate here: the backend resamples 48 kHz huddle
# audio to/from the provider's rates itself, with soxr.
)
kit.register_channel(voice)
await kit.create_room(room_id="buzz-huddle")
await kit.attach_channel("buzz-huddle", "buzz-voice")
watcher = BuzzHuddleWatcher(
kit,
voice_channel=voice,
config=BuzzConfig(relay_url="wss://...", private_key="nsec1..."),
parent_channel_id="<channel-uuid>",
room_id="buzz-huddle",
)
await watcher.start() # answers the next huddle that opens
Someone in your Buzz community clicks "start huddle", and the agent is in the call.
Limits, Honestly
- Text only for now. The channel declares threading and reactions, but rich content, media attachments, and reaction dispatch are out of scope in this release. Maximum message length is the relay's 65,536-character content limit.
- One relay channel per source. Watching several Buzz channels means several sources.
- One huddle at a time. The watcher ignores announcements that arrive while a call is in progress.
- Inbound huddle audio is unmixed. Overlapping speech from several peers arrives interleaved — acceptable for the common one-human-plus-agent huddle, not for transcribing a crowded call.
- A rejoin starts a fresh provider session. After a connection loss, the model does not remember the conversation from before the drop.
Getting Started
pip install roomkit[buzz,realtime-gemini]
BUZZ_RELAY_URL=wss://your-community.communities.buzz.xyz \
BUZZ_NSEC=nsec1... \
BUZZ_CHANNEL_ID=<channel-uuid> \
GOOGLE_API_KEY=... \
python examples/buzz_voice_agent.py
Use RoomKit 0.37.1 or later — it raises the floor to buzzkit 0.1.4, which runs all WebSocket I/O on a dedicated thread and drops late audio frames instead of bursting them, curing choppy huddle audio under event-loop load. buzzkit itself is at 0.2.1 today, adding the full message lifecycle client-side — replies, reactions, edits, deletes — plus NIP-38 user status; a plain pip install roomkit[buzz] picks it up.
The Buzz guide covers the full configuration surface, and examples/buzz_bot.py and examples/buzz_voice_agent.py in the repository are complete, runnable programs. If you try it against your own community, we'd love to hear how it goes.