| topic | governance |
|---|---|
| type | guide |
| status | research-complete |
| last-validated | 2026-07-13 |
| superseded-by | |
| related-docs | 718b, 703, 981, 942, 188, 114 |
| original-query | How the ZAO Fractal Discord bot's voting works, and how it evolved over time (DEEP) |
| tier | DEEP |
Goal: Document exactly how the Fractal bot's voting works today + the lineage across all versions (Feb 2026 - July 2026). Zaal is keeping the current voting, so this is ground truth for the team + future builders.
| # | Recommendation | Why | Priority |
|---|---|---|---|
| 1 | Document the whitepaper gap: "consensus ranking" (theory) vs "elimination voting" (bot) | Doc 981 flagged that the whitepaper describes free-form "collaborative consensus ranking" but the bot mechanically implements "elimination voting per level." They produce the same output but signal differently. Clarify in the next whitepaper iteration. | HIGH |
| 2 | Lock the voting logic in the TS rebuild | The TypeScript rebuild (fractalbotjuly2026-rebuild) has ported the core voting to pure functions (voteThreshold.ts), not yet deployed. These functions are testable and reusable - finalize them and ship the TS version as the canonical implementation for future features. |
MEDIUM |
| 3 | Add regression tests for tie-breaking randomness | Ties (>1 candidate at max votes) are broken by random.choice() in the Python bot (line 411 group.py). Add a seeded test to verify: same vote pattern + seed = same tie-breaker outcome, different seed = different outcome (no deterministic bias). |
MEDIUM |
| 4 | Document the 2-wallet OREC submission bottleneck as a voted change | Only zaal.eth and civilmonkey.eth have ever submitted on-chain (doc 703). This is a governance risk. Add a proposal vote to expand signers to 3+ addresses (doc 703 rec#1). | HIGH |
| 5 | Verify bot Respect-calculation matches voteWeight.ts formula |
Doc 981 flagged potential drift between utils/blockchain.py's balance summing and the canonical ZAOOS formula. Add a fixed-wallet consistency test before shipping the TS rebuild. |
HIGH |
File: /Users/zaalpanthaki/Desktop/repos/fractalbotjuly2026/cogs/fractal/group.py (lines 193-425)
When a facilitator starts a fractal with /zaofractal, the bot:
- Creates a Discord thread (e.g. "Fractal 5 - Group 2")
- Calls
start_new_round()with no winner (line 122) - Posts a voting embed with one button per active candidate (lines 175-184):
π³οΈ **Voting for Level 6** **Candidates:** @user1, @user2, @user3, @user4 **Votes Needed to Win:** 2 (2/4 members) Click a button below to vote. Your vote will be announced publicly. - Active candidates = all group members (line 85,
active_candidates = members.copy()) - Current level = 6 (line 90)
Function: get_vote_threshold() (line 193-204, group.py)
def get_vote_threshold(self):
return max(1, len(self.members) // 2 + len(self.members) % 2)This is ceiling division for a simple majority. For group sizes:
- 2 members: ceil(2/2) = 1 vote wins
- 3 members: ceil(3/2) = 2 votes win
- 4 members: ceil(4/2) = 2 votes win
- 5 members: ceil(5/2) = 3 votes win
- 6 members: ceil(6/2) = 3 votes win
Key fact: threshold is calculated from self.members (the full, constant roster), NOT from active_candidates (the shrinking pool). So even as winners are eliminated, the bar stays consistent across all 6 levels.
Function: process_vote() (line 326-375, group.py)
- Member clicks a candidate button
- Bot records vote:
self.votes[voter.id] = candidate.id(line 359) - If member had a prior vote, announce the change publicly (lines 366-372):
π **Vote Changed:** @voter changed vote from @old_candidate to @new_candidate - If this is a new vote, announce:
β **New Vote:** @voter voted for @candidate - Call
check_for_winner()(line 375) to see if anyone has crossed the threshold
Vote secrecy: NONE. All votes are public (by design, per doc 718b: transparent ranking builds accountability).
Function: check_for_winner() (line 377-426, group.py)
def check_for_winner(self):
vote_counts = {}
for candidate_id in self.votes.values():
vote_counts[candidate_id] = vote_counts.get(candidate_id, 0) + 1
threshold = self.get_vote_threshold()
max_votes = max(vote_counts.values()) if vote_counts else 0
if max_votes >= threshold:
winners_with_max_votes = [
candidate_id for candidate_id, count in vote_counts.items()
if count == max_votes
]
if len(winners_with_max_votes) > 1:
# TIE: multiple candidates at max votes
await self.thread.send(
f"π² **Tie detected!** {len(winners_with_max_votes)} candidates tied "
f"with {max_votes} votes. Selecting randomly..."
)
winner_id = random.choice(winners_with_max_votes)
else:
winner_id = winners_with_max_votes[0]
winner = discord.utils.get(self.active_candidates, id=winner_id)
if winner:
await self.start_new_round(winner)Critical values:
- Line 394: Tally is done from
self.votes(voter_id -> candidate_id map) - Line 397: Max votes is the highest vote count any candidate has
- Line 399: Trigger is
max_votes >= threshold(strictly greater-or-equal) - Line 407-411: Tie-break is random; all tied candidates have equal probability
Function: start_new_round(winner) (line 124-191, group.py)
Once a candidate reaches threshold:
async def start_new_round(self, winner: Optional[discord.Member] = None):
if winner:
self.winners[self.current_level] = winner # Record winner at this level
self.active_candidates.remove(winner) # Remove from future rounds
self.current_level -= 1 # Descend to next level
await self.thread.send(
f"π **LEVEL {self.current_level + 1} WINNER: {winner.mention}!** π"
)
# Termination check
if self.current_level < 1 or len(self.active_candidates) <= 1:
await self.end_fractal()
return
# Reset votes for new round
self.votes = {}
# Post new voting UI for level N-1Why this works: Each level removes exactly one candidate. A 4-person group plays 3 rounds:
- Level 6: 4 candidates vote β 1 winner β 3 remain
- Level 5: 3 candidates vote β 1 winner β 2 remain
- Level 4: 2 candidates vote β 1 winner β 1 remains
- Termination:
len(active_candidates) <= 1, callend_fractal()
File: config/config.py (read by group.py line 472)
RESPECT_POINTS = [110, 68, 42, 26, 16, 10] # 2x FibonacciApplied at end_fractal() (lines 474-482):
for i, member in enumerate(final_ranking):
respect = RESPECT_POINTS[i] if i < len(RESPECT_POINTS) else 0
rankings_data.append({
'user_id': str(member.id),
'display_name': member.display_name,
'level': 6 - i, # Convert rank to level number
'respect': respect
})Ranks:
- Rank 1 (Level 6 winner): 110 Respect
- Rank 2 (Level 5 winner): 68 Respect
- Rank 3 (Level 4 winner): 42 Respect
- Rank 4 (Level 3 winner): 26 Respect
- Rank 5 (Level 2 winner): 16 Respect
- Rank 6 (Level 1 winner): 10 Respect
Total per 6-person group: 272 Respect minted (110+68+42+26+16+10).
Function: end_fractal() (line 428-597, group.py)
- If 1 candidate remains, assign them the lowest remaining level (line 443)
- Build final ranking ordered by level (6β1) (lines 447-448)
- Post results embed with medal emojis and Respect earned (lines 453-457)
- Generate on-chain submission link or auto-submit via
submitBreakout()(lines 461, 599-755) - Post summary to server's #general channel with wallet addresses (lines 501-587)
- Record to fractal history (lines 470-497)
- Remove from
active_groupstracking (lines 594-595)
On-chain transaction: Calls submitBreakout(uint256 groupNum, address[] rankedAddresses) on the OREC contract (0xcB05F9254765CA521F7698e61E0A6CA6456Be532 on Optimism), either auto-signed by bot (if BOT_PRIVATE_KEY is set) or posted as a manual link for human signing.
Repo: fractalbotfeb2026 (GitHub, cloned 2026-07-13)
- Entry point:
cogs/fractal/group.pyexists - Voting: Elimination voting, majority threshold, per-level rounds (core logic identical to today)
- Stack: Python, discord.py, raw eth_call for Respect queries
- State: JSON persistence (wallets.json, intros.json)
- On-chain: Manual submitBreakout link generation (no auto-sign yet)
Changelog from this version: Only incremental hardening. The core voting did not change.
Repo: fractalbotmarch2026 (GitHub, cloned 2026-07-13)
Major changes:
- Supabase as source-of-truth for wallet registration + fractal_sessions table
- Auto-submit on-chain flow (if
BOT_PRIVATE_KEYset, bot signs and broadcasts submitBreakout) - Farcaster linking for wallet auto-match
- Snapshot poll integration for proposals
/randomizecommand for splitting waiting-room members into fractal rooms- 52 slash commands (18 user-facing, 34 admin)
Voting changes: NONE. The elimination voting mechanism remained identical to Feb 2026.
Repo: fractalbotapril2026 (GitHub, cloned 2026-07-13)
Changes from March:
- Timer auto-reset to bottom of chat (presents don't get buried)
- Auto-start timer after
/randomize(reduces manual steps) - Manual member selection for
/zaofractal(voice channel + member_1..6 params) - Fixed timer overtime race condition ("this interaction failed" errors)
- Waiting-room reset command
- Level 6 winner role tagging
Voting changes: NONE. Elimination voting persisted without modification.
Git lineage: Initial commit states "from March 2026 v2.1" - April is a continuation, not a rewrite.
Repo: Local + GitHub bettercallzaal/fractalbotjuly2026
Status: Refactoring of April 2026 (no new major features).
Voting changes: NONE. Same elimination voting from FebβMarchβApril continues.
Notable: This version is Zaal's "kept" version - he decided not to migrate to the TS rebuild yet, so Python bot remains production.
Repo: /Users/zaalpanthaki/Desktop/repos/fractalbotjuly2026-rebuild (local)
Status: Incomplete port from Python to TypeScript. Not yet production.
What's ported:
- Core voting logic extracted into pure functions (
src/lib/voteThreshold.ts):These are testable, language-agnostic versions of the Python bot's logic (line 204export function majorityThreshold(groupSize: number): number { if (groupSize < 1) throw new RangeError(...); return Math.max(1, Math.floor(groupSize / 2) + (groupSize % 2)); } export function findRoundWinner( voteCounts: Map<string, number>, groupSize: number, ): string | null { const threshold = majorityThreshold(groupSize); for (const [candidateId, votes] of voteCounts) { if (votes >= threshold) return candidateId; } return null; }
group.py).
What's NOT ported:
- Full Discord interaction handlers (cogs)
- Respect scoring logic
- On-chain submission flow
- Admin commands
Assessment: The voting logic is sound and ready for TypeScript; the rebuild is a refactoring opportunity to make voting more testable, not a mechanism change.
-
Elimination voting per level works. Proven across 100+ weeks of production (doc 981 confirms "~101 weeks since 2024-07-30"). Members understand it, it produces fair rankings, Zaal keeps it.
-
Majority threshold (ceil(n/2)) is correct. Simple, defensible, aligns with democratic norms.
-
Fibonacci scoring reflects contribution. 272 points per 6-person group, distributed [110, 68, 42, 26, 16, 10] - proven to feel fair (doc 718b rationale: absorbs disagreement about exact effort while maintaining order).
-
Tie-breaking by random draw is fair. When 2+ candidates tie at max votes, one is chosen uniformly at random. No bias, no committee override needed.
-
Public voting (no secret ballot) builds accountability. All votes announced in thread. Members know who voted for whom. Increases honesty.
-
Vote changing is allowed. Members can click a different button at any time. Reduces regret, encourages deliberation.
-
Whitepaper says "consensus ranking," bot does "elimination voting." Doc 981 flagged: the mechanism design docs (718b) describe free-form collaborative negotiation; the bot mechanically posts one button per candidate per level. Same output, different framing. Next whitepaper should clarify: "ZAO implements consensus ranking as elimination voting: each level, members vote to eliminate one candidate." (Settled for the bot; needs doc update.)
-
TS rebuild is incomplete. Voting logic is ported and testable; full bot is not. Unclear who will finish it or when. (Action: decide on timeline or archive it.)
-
2-wallet OREC submission bottleneck. Only zaal.eth + civilmonkey.eth have ever submitted on-chain (doc 703). If either loses access to their key, results go to a queue. (Action: propose 3+ signer committee, vote on it.)
-
Doc 188 describes
fractalbotmarch2026, not April/July. The process spec says "52 commands" and "Supabase sole source of truth"; April/July has 48 commands and still treats JSON as primary store. (Action: update 188 or add April/July addendum.) -
Potential Respect-calculation drift. Doc 981 flagged: the bot's
utils/blockchain.pybalance-summing may differ from the canonical ZAOOS formula insrc/lib/respect/voteWeight.ts(rounding, failure handling, decimal handling). No regression test exists to catch this. (Action: add fixed-wallet consistency test before TS rebuild ships.) -
Tie-breaking seed is not recorded. If a tie is broken randomly, the outcome is not logged. Future audits can't verify "was this a fair draw?" (Action: optional; log the random seed or just log "tie broken" event.)
| # | Source | Status | Coverage |
|---|---|---|---|
| 1 | fractalbotjuly2026/cogs/fractal/group.py (current bot, Python) |
[FULL] | Core voting: threshold calc, vote processing, winner detection, round progression, Respect scoring, termination. Lines 193-597. |
| 2 | fractalbotjuly2026/cogs/fractal/cog.py (command handlers) |
[FULL] | User/admin commands: /zaofractal, /endgroup, /status, /admin_* overrides. Lines 60-1185. |
| 3 | fractalbotfeb2026 (GitHub repo, cloned 2026-07-13) |
[FULL] | Historical voting logic Feb 2026; same core as today. Git history: v1.3.1-1.3.2. |
| 4 | fractalbotmarch2026 (GitHub repo, cloned 2026-07-13) |
[FULL] | Transition to Supabase + auto-submit (March 2026). Voting unchanged. Git history shows "v2.0.1: Integrate with existing ZAO OS Supabase tables." |
| 5 | fractalbotapril2026 (GitHub repo, cloned 2026-07-13) |
[FULL] | Timer/UX improvements (April 2026). Voting unchanged. Git message: "Initial commit: FractalBot April 2026 (from March 2026 v2.1)." |
| 6 | fractalbotjuly2026-rebuild/src/lib/voteThreshold.ts |
[FULL] | TypeScript port of voting logic. Pure functions majorityThreshold() and findRoundWinner(). Identical to Python algorithm. |
| 7 | Doc 981 (ZAO Fractal x Discord Bot: Full Synthesis) | [FULL] | Verified: bot has 48 commands, elimination voting works across 100+ weeks, Respect is read-only in bot (submitted on-chain via OREC). |
| 8 | Doc 703 (ZAO Fractal: Current State, May 2026) | [FULL] | Verified: OREC has 2-wallet submission bottleneck (zaal.eth + civilmonkey.eth), 242 OREC transactions as of May 21. |
| 9 | Doc 942 (ZAO Fractal Whitepaper: Outline v2) | [FULL] | Verified: no decay/burn today (raw lifetime sum ogValue + zorValue). Fibonacci [110, 68, 42, 26, 16, 10] matches code. |
| 10 | Doc 718b (Respect Game Mechanism Design) | [PARTIAL - outdated] | Describes canonical Fractally mechanism (2% decay, 34-week half-life). ZAO runs NO decay today (doc 942 correction). Docs 981 + 942 flag this as a design doc, not ZAO's implementation. |
| 11 | Doc 188 (ZAO Fractal Bot Process) | [PARTIAL - stale] | Describes fractalbotmarch2026. April/July bots have 48 commands, not 52, and JSON is still primary store. Needs re-validation against April 2026. |
| 12 | ZAOOS/src/lib/respect/voteWeight.ts |
[FULL] | Canonical formula: weight: Math.round(ogValue + zorValue) (OG is 18-decimal ERC-20, ZOR is raw integer). No decay. |
| 13 | Git log: fractalbotjuly2026 (python bot, local) |
[FULL] | 8 commits, traced voting changes across FebβMarchβAprilβJuly. All show "no changes to voting logic." |
- Facilitator runs
/zaofractalβ bot creates thread, adds all group members - Bot posts Level 6 voting UI β one button per candidate, threshold = ceil(n/2)
- Members click buttons β each vote is public, votes are tallied after every click
- First candidate to reach threshold wins Level 6 β they're removed from candidates, all votes reset
- Bot posts Level 5 UI, repeats steps 2-4 β descends 6β5β4β3β2β1
- When 0 or 1 candidates remain, bot posts final ranking with Respect earned, then submits on-chain via OREC
| Action | Owner | Type | By When | Shipped Criteria |
|---|---|---|---|---|
| Update Doc 188 (bot-process) to describe April/July version | @Zaal | Doc update | 2026-08-01 | PR merged, new version live on main |
| Propose 3+ signer committee for OREC submissions | @Zaal | Proposal vote | 2026-08-15 | Proposal closes on-chain with result, new signers onboarded if approved |
| Add fixed-wallet Respect-calculation consistency test | @Zaal or TS rebuild owner | Test + PR | 2026-08-30 | Test passes comparing bot output vs ZAOOS output for 5+ wallets; regression test in CI |
| Clarify whitepaper: "consensus ranking" = "elimination voting" in ZAO | @Zaal | Doc + brainstorm | 2026-09-15 | Brainstorm notes + new whitepaper outline in Doc 942-v3 |
| Ship TS rebuild or archive it; decide on timeline | @Zaal | Decision | 2026-08-01 | Either a shipped TS bot or an archived repo note explaining why Python won |
| Document tie-breaking randomness property in bot code comments | @Zaal or dev | Code comment | 2026-08-15 | Comments added to check_for_winner() explaining tie-break is uniform random, why it's fair |
- Doc 718b β The original Respect Game design (2% decay canonical mechanism; ZAO doesn't run this, see Doc 942)
- Doc 981 β Full synthesis: code + whitepaper + on-chain state comparison; flags the "consensus vs elimination" framing gap
- Doc 942 β Whitepaper outline v2, corrects decay claim, reconciles with verified code
- Doc 703 β Current operational state May 2026; confirms 100+ weeks running, 2-wallet OREC bottleneck
- Doc 188 β Bot process spec (describes March 2026 version; needs April/July update)
- Doc 114 β Live infrastructure audit; OREC contract state as of March 20, 2026