DEV Community

Oren Rot
Oren Rot

Posted on

Recomputing an OpenAgentForum poll result yourself: 3 signed ballots, 1 matching tally

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. The details below come from the project's public onboarding manual (https://openagentforum.com/agent.md) and from a script I ran against the live public record on October 20, 2026.

OpenAgentForum polls are ordinary signed messages. A poll envelope opens one and each vote envelope is a ballot bound to it by the poll's id and checksum. The manual says anyone can recompute the count instead of trusting the hub. I did that for the one poll on the public ledger.

The poll

"What should this network build next?" is an open-electorate, advisory poll with a plurality rule. It is closed. The hub's summary reports counts of [1, 2, 0, 0] across four options, with 3 valid ballots.

What the script does

  1. Fetches the poll from GET /v1/polls and its envelope from GET /v1/polls/{id}.
  2. Checks the poll envelope's checksum and Ed25519 signature, using the sender's key from GET /v1/agents/{sender}. The signing rules are the ones in the manual: canonical JSON with keys sorted by UTF-16 code units, then id|channel|sender|type|sequence|timestamp|checksum signed with Ed25519.
  3. Pulls vote envelopes for that poll id from the general channel.
  4. Keeps a ballot only if its checksum and signature verify, its pollHash equals the poll's checksum, and its timestamp is before the deadline. It keeps the latest ballot per voter, because the poll's revote setting is latest.
  5. Counts the choices and compares them with the hub.
import json, hashlib, urllib.request
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
from cryptography.exceptions import InvalidSignature
H={"User-Agent":"SwarmRelay-Agent/1.0"}
def get(u): return json.load(urllib.request.urlopen(urllib.request.Request(u,headers=H)))
def canon(v):
    if v is None or isinstance(v,(bool,str)): return json.dumps(v,ensure_ascii=False)
    if isinstance(v,(int,float)): return json.dumps(v)
    if isinstance(v,list): return "["+",".join(canon(x) for x in v)+"]"
    return "{"+",".join(json.dumps(k,ensure_ascii=False)+":"+canon(v[k]) for k in sorted(v,key=lambda s:s.encode("utf-16-be")))+"}"
def verify(e,pub):
    ck=hashlib.sha256(canon(e["payload"]).encode()).hexdigest()==e["checksum"]
    s="|".join(str(e[k]) for k in ["id","channel","sender","type","sequence","timestamp","checksum"])
    try: Ed25519PublicKey.from_public_bytes(bytes.fromhex(pub)).verify(bytes.fromhex(e["signature"]),s.encode()); sg=True
    except InvalidSignature: sg=False
    return ck and sg
poll=get("https://openagentforum.com/v1/polls")["polls"][0]
print("poll:",poll["title"]); print("hub counts:",poll["counts"],"tallyId",poll["tallyId"][:12])

pid=poll["pollId"]
det=get("https://openagentforum.com/v1/polls/"+pid)
print("poll envelope verifies:", verify(det["poll"], get("https://openagentforum.com/v1/agents/"+det["poll"]["sender"])["agent"]["publicKey"]))

page=get("https://openagentforum.com/v1/channels/general/messages?limit=30&after=100")["messages"]
votes=[m for m in page if m["type"]=="vote" and m["payload"].get("pollId")==pid]
print("vote envelopes found:",len(votes))
latest={}
for v in sorted(votes,key=lambda m:m["storedSeq"]):
    pub=get("https://openagentforum.com/v1/agents/"+v["sender"])["agent"]["publicKey"]
    ok=verify(v,pub); hash_ok=v["payload"].get("pollHash")==poll["pollHash"]
    print(" ",v["sender"],"choice",v["payload"]["choice"],"verified",ok,"pollHash_matches",hash_ok)
    if ok and hash_ok and v["timestamp"]<=poll["deadlineAt"]: latest[v["sender"]]=v["payload"]["choice"]
counts=[0]*len(det["poll"]["payload"]["options"])
for c in latest.values(): counts[c]+=1
print("my counts:",counts,"hub counts:",poll["counts"],"match:",counts==poll["counts"])
Enter fullscreen mode Exit fullscreen mode

Result

poll: What should this network build next?
hub counts: [1, 2, 0, 0] tallyId 3e8d9bd8f3da
poll envelope verifies: True
vote envelopes found: 3
  agent_e32219c73bc3da8e choice 0 verified True pollHash_matches True
  agent_dab292acbe7f8ac5 choice 1 verified True pollHash_matches True
  agent_5ddbf66022a82fb2 choice 1 verified True pollHash_matches True
my counts: [1, 2, 0, 0] hub counts: [1, 2, 0, 0] match: True
Enter fullscreen mode Exit fullscreen mode

My count matched the hub's: option 1 ("Wake hooks for reactive agents (RFC 0002)") wins with 2 of 3 ballots.

What this does not prove

  • The manual says the deadline is enforced by the relay when a ballot is ingested. My script checks the signed timestamp, which the voter chose. It is a sanity check and not the same thing.
  • I read the first 30 messages after storage sequence 100. That was enough for this poll. A larger poll needs real paging.
  • The hub reports an electorate basis of "registry-trusted". I did not reimplement that check. The project also ships a CLI command for this (npx swarmrelay tally general <pollId>), and I did not run it. Use it for anything that matters.
  • It is a single poll with three ballots and an advisory result. It shows the recount works. It says nothing about how the network behaves at scale.

Top comments (0)