Skip to main content

Board Council Demo

Overview​

The Board Council is an end-to-end governance demo on ULedger. It demonstrates the hybrid council governance pattern: a group of human principals each delegates an AI voting agent; agents vote autonomously within a time window; a human chair finalizes the outcome on-chain.

Every action (proposal creation, agent authorization, vote submission, finalization) is a signed transaction on the ML-DSA-87 chain. The complete decision trail is on-chain, STARK-proofed, and auditable.

Stack:

  • Frontend: Preact TypeScript app (Vite, port 5177)
  • Board driver: Go backend (port 8277): connects to the ULedger cluster and manages state
  • Blockchain: ULedger node cluster (9 nodes, ML-DSA-87 chain)
  • Contract: council-voting.wat (WASM smart contract deployed by the driver on startup)

Architecture​

Browser
│ HTTPS
▼
Vite Dev Server (port 5177)
│ /api/* proxy → ULEDGER_NODE_URL (default http://localhost:8090)
│ /board/* proxy → Board Driver (default http://127.0.0.1:8277)
▼
Board Driver (Go, port 8277)
│
├── GET /state → BoardState (proposals, phase, chain height, nodes)
├── GET /events → SSE stream (vote, new_proposal, finalized, setup_done)
├── POST /proposals → create proposal
└── POST /proposals/{id}/vote → cast human vote
│
▼
ULedger Cluster (9 nodes, docker, ML-DSA-87)
│
├── council-voting.wat (deployed contract)
├── 6 member wallets (human principals)
└── 6 agent wallets (AI directors)

Participants​

RoleIdentityDescription
Chairyou (member:alice)Human. Creates proposals. Casts the final human vote.
Director 1agent:aliceAI agent delegated by alice. Votes autonomously.
Director 2agent:bobAI agent delegated by bob. Votes autonomously.
Director 3agent:charlieAI agent delegated by charlie. Votes autonomously.
Director 4agent:daveAI agent delegated by dave. Votes autonomously.
Director 5agent:eveAI agent delegated by eve. Votes autonomously.
Director 6agent:frankAI agent delegated by frank. Votes autonomously.

Setup: What Happens on Driver Start​

When the board driver starts, it performs the full council setup sequence automatically:

1. Connect to 9-node Docker cluster (discover ephemeral ports)
2. Register wallets (6 member wallets + 6 agent wallets)
3. Deploy council-voting.wat contract
4. addMember(alice), addMember(bob), ..., addMember(frank) [6 txs]
5. alice: authorizeAgent(agent:alice)
bob: authorizeAgent(agent:bob)
... [6 txs]
6. Phase → "ready"
7. Emit "setup_done" SSE event

If runs/contract-addr.txt exists, step 3 is skipped and the previously deployed contract is reused.


Proposal Lifecycle​

Step 1: Create Proposal (Human Chair)​

The chair types a prompt in the frontend and submits a proposal:

POST /board/proposals
{ "prompt": "Should we expand our product line into the APAC market?" }

The driver:

  1. Creates the proposal in the contract: createProposal(title, description, 10, 2) (10-block voting window, quorum 2).
  2. Issues work tokens to all 6 agents: issueWorkToken(agent:alice, proposalId) × 6.
  3. Emits new_proposal SSE event to all browser clients.
{
"id": "1",
"title": "APAC Expansion",
"description": "Should we expand our product line into the APAC market?",
"status": "active",
"endBlock": 12345,
"createBlock": 12335
}

Step 2: AI Directors Vote (Autonomous)​

Each agent (LLM or automated process) receives the proposal and votes:

submitWork(proposalId, vote=1, evidence="Market analysis shows 23% growth potential in APAC...")

The driver orchestrates agent voting concurrently. Each vote is a separate signed transaction from a separate agent wallet. The on-chain record shows:

block 12336: agent:alice submitted vote=YES, evidence="..."
block 12337: agent:bob submitted vote=YES, evidence="..."
block 12337: agent:charlie submitted vote=NO, evidence="..."
block 12338: agent:dave submitted vote=YES, evidence="..."
block 12339: agent:eve submitted vote=ABSTAIN, evidence="..."
block 12340: agent:frank submitted vote=YES, evidence="..."

Every vote is attributed to the agent wallet, which is attributable to the member wallet that authorized it, which is attributable to the PERSONAL entity (human) in the hierarchy.

The driver emits a vote SSE event for each confirmed vote:

{
"type": "vote",
"proposalId": "1",
"agent": "agent:alice",
"vote": "YES"
}

Step 3: Human Chair Votes​

The frontend shows the human chair a YES / NO / ABSTAIN button. This is a human-signed vote:

POST /board/proposals/1/vote
{ "choice": 1 }

The driver submits submitWork(proposalId, 1, "Human vote") from the alice-wallet (the member wallet, not the agent wallet; the chair votes directly). This is the human-in-the-loop intervention point.

The vote SSE event for the human vote includes "agent": "you"; the frontend marks this vote as confirmed when it receives the event.

Step 4: Advance to Deadline​

The voting window is 10 blocks. The driver advances the chain past endBlock by submitting no-op isMember() calls that drive block production.

Step 5: Finalize (Human or Automated)​

Once the deadline is passed, any party can finalize:

finalizeProposal(proposalId)

The contract tallies votes and sets status:

  • If yes votes ≥ quorum: "passed"
  • If no votes ≥ quorum: "failed"
  • Otherwise: "failed" (insufficient quorum)

The driver reads the final tally from contract state and emits a finalized SSE event:

{
"type": "finalized",
"proposalId": "1",
"status": "passed",
"tally": { "yes": 5, "no": 1, "abstain": 1 }
}

The frontend updates the proposal card to show the final result, tally bars, and timestamp.


Frontend Components​

Displays all 6 members with their vote status for the active proposal:

┌─────────────────────────┐
│ Members │
│ │
│ You (chair-1) [YES] │
│ Bob (director-1) [YES] │
│ Charlie (dir-2) [NO] │
│ Dave (director-3)[YES] │
│ Eve (director-4) [---] │
│ Frank (dir-5) [YES] │
│ │
│ [+ New Proposal] │
└─────────────────────────┘

Main: Active Proposal Card​

┌─────────────────────────────────────────────────────┐
│ Active Proposal #1 Expires block 12345 6 votes │
│ │
│ APAC Expansion │
│ Should we expand our product line into the APAC... │
│ │
│ Block: ██████████░░░░░ 10/10 │
│ │
│ AI Reasoning: Market analysis shows 23% growth... │
│ │
│ Tally: │
│ YES ████████████░░░ 5 │
│ NO ████░░░░░░░░░░░ 1 │
│ ABS ██░░░░░░░░░░░░░ 1 │
│ │
│ [YES] [NO] [ABSTAIN] │
└─────────────────────────────────────────────────────┘

Setup Overlay​

If phase != "ready", a full-screen overlay shows:

┌─────────────────────────────────────────────────────┐
│ │
│ Initialising... │
│ Setting up council on ML-DSA-87 chain │
│ │
└─────────────────────────────────────────────────────┘

Running the Demo​

Prerequisites​

  • Docker cluster running: node-v2-uln-debug-1 through node-v2-uln-debug-9
  • Go 1.24+
  • Node 20+ with pnpm

Start the Board Driver​

cd /home/dr/uledger/uledger-apps/board-council/tools/board-driver
go run . 2>&1 | tee /tmp/board-council-driver.log

Driver flags:

  • -sdk-root <path>: path to golang-sdk (auto-detected if not set)
  • -state-dir <path>: directory for contract-addr.txt and votes.json (default: ./runs)
  • -listen <addr>: bind address (default: 127.0.0.1:8277)

Start the Frontend​

cd /home/dr/uledger/uledger-apps/board-council
pnpm dev
# → http://localhost:5177

Troubleshooting​

SymptomFix
No containers foundStart the Docker cluster first.
Driver offline (frontend shows error)Start the board driver.
Stuck on "Initialising" + TRANSACTION_ERRORDelete runs/contract-addr.txt and restart driver.
Wallet load errorWrong -sdk-root or wrong working directory.

On-Chain State After a Full Demo Run​

After a complete proposal lifecycle, the on-chain state includes:

contract storage:
"council:member:<alice-addr>" → "1" (6 entries)
"agent:principal:<alice-agent>" → "1" (6 entries)
"proposal:1:status" → "passed"
"proposal:1:title" → "APAC Expansion"
"proposal:1:votes:yes" → "5"
"proposal:1:votes:no" → "1"
"proposal:1:votes:abstain" → "1"
"voted:1:<alice-addr>" → "1" (7 entries: 6 agents + 1 human)
"token:<alice-addr>:1:agent" → "1" (6 work token entries)

Every on-chain write is in the transaction receipts, Merkle-chained, and covered by the Helix STARK checkpoint for its epoch.

For the full council-voting.wat function reference (signatures, state keys, access control rules) see The Council Voting Contract.


What This Demo Proves​

  1. Autonomous agent voting is on-chain: Each agent vote is a signed transaction from a specific agent wallet address.
  2. Human authorization is traceable: Each agent's vote is traceable to the member wallet that called authorizeAgent, and to the PERSONAL entity (human) above it in the hierarchy.
  3. Human intervention is explicit: The chair's vote is a separate, human-signed transaction; it is distinguishable from agent votes on-chain.
  4. Finalization is provable: The contract's finalizeProposal call produces a deterministic, on-chain result. The tally is derived from the votes, not from an off-chain system.
  5. The audit trail is complete: From council setup through finalization, every action is a signed transaction. The entire governance event is reconstructable from on-chain data alone.