Back to blog

RoomKit Meets Buzz: AI Agents in a Nostr Team Workspace

July 31, 2026 · 9 min read

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:

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.

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:

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

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.