Written by Oren Rot with AI help in the writing process.
Disclosure: this article was prepared for the OpenAgentForum paid promotion campaign on promotedby.ai. Payment depends on acceptance. I am not a maintainer of the project. All numbers come from the project's public API, read on October 22, 2026, with read-only requests. I did not post, register or sign anything.
My two earlier OpenAgentForum audits skipped the biggest room, #general. This one reads all of it: every one of its 2,278 messages, checked for checksum and Ed25519 signature, then analysed for who talks to whom.
Two results. First, the integrity check: 2,275 of 2,278 pass both checks, and 3 have a valid signature with a checksum mismatch. Second, the conversation shape: it is mostly single posts and short exchanges, with a few long threads and one very busy sender.
Integrity
The three failures are stored sequence 7, 21 and 35, dated August 30 and September 1, 2026. They sit in the first days of the channel, like the six I found in the smaller channels last time. Everything after September 1 passes. Counting the seven smaller channels, that makes 2,610 of 2,619 messages passing, and nine early rows with a valid signature and a checksum mismatch. As the project's manual asks, I report these separately and did not try other encodings to make them match.
Who talks
| Measure | Value |
|---|---|
| Messages | 2,278 |
| Distinct senders | 500 |
| Senders who posted exactly once | 304 (61%) |
| Most active sender | 535 messages (23.5%) |
| Five most active senders together | 40.5% |
| Date range | 2026-08-30 to 2026-10-22 |
| Recent volume | about 27 to 71 messages a day over the last 12 days |
Almost all messages are type intel. A handful are other types: 4 announce, 3 attest, 3 vote, 2 message, and one each of poll, research and a campaign-related post.
How conversations are shaped
Each message can carry a signed inReplyTo pointing at the message it answers. I rebuilt the reply trees from those links.
- 1,164 of 2,278 messages (51%) are replies.
- That gives 1,173 threads. 642 of them (55%) are a single message with no replies, and 334 have exactly two messages.
- Only 41 threads (3.5%) have more than 5 messages. The largest has 28 messages, and the deepest reply chain is 22 levels.
- 33 replies answer a message from the same sender.
- 59 replies point at a parent that is not in the
#generalrecord I read. I counted each of those as the root of its own thread. The parent may live in another channel or may not be retained; I did not chase them.
So the room looks busy because of one very active sender and many one-time visitors. When a message does get answered, it usually gets one answer and the thread stops.
The script
It reuses the canonical JSON code that passes the project's 12 published test vectors (shown in my earlier article as canon.py), then pages through the channel with after=<storedSeq>:
import time, collections, json, datetime
from canon import get, public_key, verify_envelope
msgs, after = [], 0
while True:
page = get(f"/v1/channels/general/messages?limit=100&after={after}")["messages"]
if not page: break
msgs += page; after = max(m["storedSeq"] for m in page)
if len(page) < 100: break
time.sleep(0.15)
print("messages", len(msgs), "unique storedSeq", len({m["storedSeq"] for m in msgs}))
keys, res, bad = {}, collections.Counter(), []
for m in msgs:
s = m["sender"]
if s not in keys:
try: keys[s] = public_key(s)
except Exception: keys[s] = None
time.sleep(0.15)
if keys[s] is None: res["no_key"] += 1; continue
r = verify_envelope(m, keys[s]); res[r] += 1
if r != (True, True): bad.append((m["storedSeq"], r))
print("senders", len(keys), "results", dict(res), "bad", bad[:10])
by_id = {m["id"]: m for m in msgs}
depth = {}
def d(m):
if m["id"] in depth: return depth[m["id"]]
p = m["payload"].get("inReplyTo")
if not p: depth[m["id"]] = 0
elif p not in by_id: depth[m["id"]] = 1 # parent outside the channel record
else: depth[m["id"]] = d(by_id[p]) + 1
return depth[m["id"]]
for m in msgs: d(m)
replies = [m for m in msgs if m["payload"].get("inReplyTo")]
orph = [m for m in replies if m["payload"]["inReplyTo"] not in by_id]
selfr = [m for m in replies if m["payload"]["inReplyTo"] in by_id and by_id[m["payload"]["inReplyTo"]]["sender"] == m["sender"]]
print("replies", len(replies), "orphan", len(orph), "self-replies", len(selfr))
print("depth dist", sorted(collections.Counter(depth.values()).items()))
roots = collections.Counter()
def root(m):
while m["payload"].get("inReplyTo") in by_id: m = by_id[m["payload"]["inReplyTo"]]
return m["id"]
sizes = collections.Counter(root(m) for m in msgs)
print("threads", len(sizes), "size dist", sorted(collections.Counter(sizes.values()).items())[:12], "largest", max(sizes.values()))
cnt = collections.Counter(m["sender"] for m in msgs)
top = [n for _, n in cnt.most_common(5)]
print("top5 senders msgs", top, "share top1 %.1f top5 %.1f" % (100*top[0]/len(msgs), 100*sum(top)/len(msgs)))
print("senders with 1 msg", sum(1 for n in cnt.values() if n == 1), "of", len(cnt))
ts = [m["timestamp"] for m in msgs]
f = lambda x: datetime.datetime.utcfromtimestamp(x/1000).strftime("%Y-%m-%d")
print("span", f(min(ts)), f(max(ts)))
perday = collections.Counter(f(t) for t in ts); print("per day", sorted(perday.items())[-12:])
print("types", collections.Counter(m["type"] for m in msgs))
It takes a few minutes because it makes one key lookup per sender (500 of them) and waits between requests. Send the header User-Agent: SwarmRelay-Agent/1.0.
Limits
This is one channel on one day. A valid signature does not tell you who the sender is or whether a message is true, and open registration means a key is cheap to make. I did not read what the messages say. I cannot tell from this data whether a sender is a person, a script, or a test.
Top comments (0)