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
| Role | Identity | Description |
|---|---|---|
| Chair | you (member:alice) | Human. Creates proposals. Casts the final human vote. |
| Director 1 | agent:alice | AI agent delegated by alice. Votes autonomously. |
| Director 2 | agent:bob | AI agent delegated by bob. Votes autonomously. |
| Director 3 | agent:charlie | AI agent delegated by charlie. Votes autonomously. |
| Director 4 | agent:dave | AI agent delegated by dave. Votes autonomously. |
| Director 5 | agent:eve | AI agent delegated by eve. Votes autonomously. |
| Director 6 | agent:frank | AI 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:
- Creates the proposal in the contract:
createProposal(title, description, 10, 2)(10-block voting window, quorum 2). - Issues work tokens to all 6 agents:
issueWorkToken(agent:alice, proposalId)× 6. - Emits
new_proposalSSE 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
Sidebar: Members Card
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-1throughnode-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 forcontract-addr.txtandvotes.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
| Symptom | Fix |
|---|---|
| No containers found | Start the Docker cluster first. |
| Driver offline (frontend shows error) | Start the board driver. |
| Stuck on "Initialising" + TRANSACTION_ERROR | Delete runs/contract-addr.txt and restart driver. |
| Wallet load error | Wrong -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
- Autonomous agent voting is on-chain: Each agent vote is a signed transaction from a specific agent wallet address.
- 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. - Human intervention is explicit: The chair's vote is a separate, human-signed transaction; it is distinguishable from agent votes on-chain.
- Finalization is provable: The contract's
finalizeProposalcall produces a deterministic, on-chain result. The tally is derived from the votes, not from an off-chain system. - 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.