Blog

August 15, 2026 · 10 min read

Building an AI Support Center on Buzz: Six Agents in Human Rooms

Self-hosting the Buzz Nostr relay and attaching six AI agents with different roles over the same protocol people use. Why Buzz, and what changes once it is deployed.

  • Buzz
  • AI agents
  • support center
  • Nostr
  • self-hosting

When I build an app alone, customer support directly takes time away from development. App Store reviews accumulate, mail arrives, and questions appear in the community. Slow replies come back as ratings.

I was not ready to hire someone, and attaching a commercial help desk would put conversations in someone else's store. So I built the support center myself. I put one relay in the cluster and brought six AI agents with different roles into the same rooms as people.

The path from an incoming request to an outgoing reply

At a glance

Symptom Reviews, mail, and community questions arrive at once while there is only one person to answer them
Cause A commercial help desk stores conversations in someone else's system, and connecting AI is limited to what the vendor exposes
Fix Self-host the Buzz relay in the cluster and connect six agents with different roles over the same protocol as people. Make every external write pass both an environment gate and human approval
Result Eleven workloads have run continuously for 21 hours, and the 16-locale reply path and approval ledger are enforced in code rather than prompts

There were two reasons not to use a commercial help desk

The first was ownership. Customer requests and reply history are some of the longest-lived product assets. If they accumulate in a commercial service, a later migration can bring out only what its export feature allows. Thread structure, who approved what, and the reasoning behind a reply disappear if they fall outside that scope.

The second was how to attach AI. Most help desks offer AI as a feature: a summary button, reply suggestions, and sentiment analysis. I wanted agents to enter rooms like people, talk, look things up, request approval, and leave those records behind. There was no reason to wait for a vendor to open that place.

I chose Buzz because a relay is a workspace

Buzz is Block's Apache-2.0 open-source Rust workspace. In one sentence, it is a self-hosted collaboration space where people and AI agents use the same rooms.

The core design is stated directly in its project description.

Every message, reaction, workflow step, review approval, and git event is a signed event in one log. Whether the author is a person or a process, it has the same shape, the same identity model, and the same audit trail.

That sentence contained everything I needed. Opening a separate management API for agents would put that path outside the audit boundary. In Buzz, an agent has its own key, its own channel membership, and its own audit trail. Scope is set by identity, not a permission flag. It is the same way I treat a team member.

The practical strengths that decided it were:

Strength What changes
Relay as workspace One domain is one workspace; there is no need to wire together five services
Event log as source of truth Human and agent messages are the same kind; one place is enough for auditing
Identity-based scope Each agent has different keys and membership; permissions are not explained in prompts
Self-hosting Relay, storage, and search index stay in our cluster
Helm chart provided Postgres, Redis, and MinIO come bundled; it starts within a day
Apache-2.0 No need to wait for a vendor roadmap; I can read a crate when needed

The 30 crates separate the relay, authentication, agent adapter, and push gateway. docs/nips/ contains 16 drafts defining what standard Nostr does not cover. I can confirm the required behavior in the documentation first, then compare it with the implementation.

The team has six members and separate rooms

I divided the roles into six. I did not build one universal agent that does everything.

Name Role Room Main work
Haram Coordinator care-lobby Briefings, escalation, approval coordination
Ria Reviews app-reviews App Store replies in 16 locales, rating management
Daon Community community Forum operations, FAQ, report handling
Woojin Mail mail-desk Mail tickets, first replies
Sena Metrics analytics-kpi App analytics, KPI and CSAT reports
Taesan Operations cluster-ops Cluster and service state, first incident response

All six use the same model and have Korean fixed as their output language. Each publishes a profile event and has an avatar. In the desktop client, they sit beside people.

The reason for dividing roles is not capability distribution but permission distribution. The review agent can reply in the App Store and cannot send mail. The coordinator and metrics agent cannot write anything. They read, decide, and hand off to a person.

The reason for dividing rooms is routing. App Store reviews enter the review room and mail enters the mail room. An agent that is not responsible is not a member of that room and does not receive its events. The channel list is the work assignment.

Deployment has three steps

It was running within a day. The order matters.

First, deploy the relay with Helm. The chart bundles Postgres, Redis, and MinIO. Put the relay URL and owner public key in the values file and it is done. I use the published image as-is; there is no relay build.

Second, create secrets and the ledger database. There are three groups: agent keys, external-service credentials, and ledger connection information. A person applies secrets and cluster changes; agents create manifests only.

Third, deploy the agents. One script resolves channel identifiers, the Ingress address, and the approver list, then creates six Deployments.

The current cluster count is:

Workload Count
Relay 1
Postgres · Redis · MinIO 3
Agents 6
Ledger database 1

This is the relay event distribution after 21 hours of continuous operation: 59 channel messages, 37 profiles, 23 each of channel metadata, admin, and member events, and six channel-creation events.

The 59 channel messages matter. A message sent by a person from the desktop and one sent by an agent are the same kind of event. One query shows who said what.

Permissions diverge at registration, not in prompts

There is one point where agents touch the outside world. Rather than giving each of the six its own API client, they pass through one MCP server. It connects to the App Store, community forum, mail server, cluster-state reads, and ledger database.

Permissions diverge in the tool list. A tool absent from a role's allowlist is not registered in the session. Because the model does not know it exists, there is no call attempt and no rejection log for a person to inspect.

An action that leaves a trace outside passes through two layers.

Gate Default What opening it enables
Environment flag Closed Register App Store replies, community replies, and mail
Cluster write flag Closed Restart workloads
Approver list Empty = block everything Public keys of people with approval authority

The environment flag must be open and someone on the approver list must approve. Neither one alone sends anything. This rule is in the MCP server code, not a prompt sentence.

Block 16 locales before the call

App Store replies use the language of the release territory. App Store Connect measured 16 locales.

ko  ja  zh-Hans  zh-Hant  en-US  de-DE  fr-FR  es-ES
it  nl-NL  pt-BR  tr  vi  da  no  sv

There is an easy mistake here. App Store territory codes have three letters, while locales are based on two-letter language codes. An unmapped territory is sent in English with a fallback marker.

More important is what happens when the language differs. If the reply body does not match the target locale, it is rejected before App Store Connect is called. The path that would send a Korean reply to a French review is blocked in code. Even if the model makes a mistake, it does not leave the system.

Successful replies are recorded in the ledger: what was sent, in which language, and when.

Leave the ledger where a person can verify it

When work is handed to an agent, there must be a way to check what it did. So I created ledger tables in a separate database.

Table What it contains
KPI targets Target values and periods for each metric
KPI samples Measurements at each point in time
CSAT responses Satisfaction scores by channel and locale
Review replies Replies actually sent to the App Store
Approval requests Who requested what and who approved it
Ticket events State changes for mail and requests
SLA policies Response-time targets by channel

I registered an initial target of a 4.5 monthly average customer-satisfaction score. The current ledger data exists to check the flow: three approval requests, three satisfaction responses, and one ticket verify that the path runs end to end. Real aggregation fills in as operations accumulate.

I created the ledger first because of order. If metrics are added later, there are no records before that point. A place to record must exist before an agent sends its first reply.

What I gained from this structure

Four changes appeared after deployment.

Conversations remain inside our cluster. The relay, storage, and search index are all self-hosted. If I move to another tool later, I still have the event log.

Agent work remains beside human work. Auditing does not require two places. Open a channel and human decisions and agent lookups appear together in time order.

Permissions are enforced instead of explained. Writing "you only read" in a prompt and having no tool available are different. The latter leaves no way to disobey the instruction.

Adding one does not touch the other five. To add a role, I add its allowlist and channel membership. The other five configurations remain unchanged.

The last item is the largest benefit for someone working alone. When the cost of adding one stays constant, it remains possible to grow.

If you hit this

  • Where do customer conversations accumulate? Can you export thread structure and approval history later?
  • Do AI agents have a separate management API, or do they use the same protocol as people?
  • Are role permissions written in prompts, or enforced at tool registration?
  • Is human approval enforced in code for actions that leave an external trace?
  • For multilingual replies, do you block a language mismatch before the external call?
  • Did you create a place to record metrics before the first reply?