Technical Architecture

System Architecture

A multi-layered architecture combining client-side ZK proofs, an NestJS backend with 48 modules and 22 extracted packages, 13 OpenZeppelin v5 smart contracts across five networks, and a ZK rollup prover cluster.

Three-Layer Architecture at Scale

Agora is built on a three-layer architecture that separates concerns, maximizes security, and scales independently. The layers are connected by an event-driven rollup pipeline and a shared schema package so changes propagate safely across web, mobile, and backend:

  • Frontend: Next.js 15 web + React Native 0.81.5 mobile. Client-side proof generation, ZK identity management, offline quota tracking.
  • Backend: NestJS 11 with 48 modules and 22 extracted @agora/* packages. PostgreSQL, Redis, Bull Queue, Socket.IO, analytics engine with k-anonymity.
  • Blockchain: 13 Solidity 0.8.27 contracts on OpenZeppelin v5. Deployed across Ethereum + Base + Arbitrum + Optimism + Polygon.

An off-chain Gnark prover cluster (Go, gRPC:50051, 8 CPU / 32 GB RAM) batches ZK proofs to ZKRollupDA.sol. A Canton validator integration provides private settlement for enterprise customers. Prometheus + Grafana monitor every critical path.

Frontend Layer

Web and mobile clients generate ZK proofs locally and never send secrets to the server. 40+ web routes, 18 mobile feature modules.

Next.js 15React 19React Native 0.81.5Expo 54Framer MotionTailwind CSSRTK QueryPoseidon2

Key Responsibilities:

  • Client-side ZK proof generation (Semaphore, RLN, UniRep, Groth16)
  • Global + per-community ZK identity (SecureStore + Poseidon2 on mobile)
  • WebView-based Groth16 proof generation for RLN and Semaphore batch circuits
  • Offline quota tracking, real-time updates via Socket.IO
  • Three theme variants: Modern neon, Classic Light, Classic Dark

Backend Layer

NestJS monorepo with 48 modules and 22 extracted core packages. Runs the API, orchestrates ZK proof batching, drives analytics, and anchors on-chain state.

NestJS 11TypeScriptPostgreSQLRedisTypeORMBull QueueSocket.IO

Key Responsibilities:

  • 48 domain modules: polls, votes, posts, communities, messages, wallet, staking, governance, analytics, moderation, RLN, UniRep, ZK rollup, Canton
  • 22 extracted packages under @agora/* (auth, zk-engine, token-engine, messaging, analytics, realtime, anon-credential, anon-receipt…)
  • Merkle tree construction, batch proof queueing, nullifier indexing
  • Event store with schema registry and k-anonymity engine (k ≥ 25)
  • Real-time messaging, Socket.IO backpressure handling

Blockchain Layer

13 Solidity 0.8.27 smart contracts built on OpenZeppelin v5, deployed across Ethereum mainnet plus four L2 networks. All token, staking, vesting, and governance operations are fully on-chain.

Solidity 0.8.27Cancun EVMOpenZeppelin v5HardhatEthereumBaseArbitrumOptimismPolygon

Key Responsibilities:

  • 7 AGR token contracts: Token, Staking, Vesting, Governor, Timelock, Treasury, BatchPayout
  • 4 polling contracts: PollManager v1/v2/v3 + Groth16Verifier for on-chain proof verification
  • 2 advanced contracts: ZKRollupDA (batch state commitments) + PollToken (per-poll custom tokens)
  • Multi-sig tiered treasury approval, ERC20Votes snapshots, nullifier enforcement
  • Compiled with optimizer runs=200; deployed via Hardhat scripts with role granting

ZK Stack — Semaphore + RLN + UniRep + Groth16

Agora combines four ZK primitives. Semaphore v4 handles group membership. RLN provides rate-limiting nullifiers so anonymous posts cannot be spammed. UniRep adds epoch-based pseudonymous reputation. Groth16 verifies everything on-chain. Six custom circom circuits extend this foundation.

semaphore_batch.circom

Batch Semaphore proofs — multiple votes in a single transaction

rln.circom

Rate-limiting nullifier for anti-spam in anonymous posts/comments

reputation_proof.circom

Prove UniRep reputation ≥ threshold without revealing identity

epoch_key.circom

UniRep epoch key generation for pseudonymous actions

state_transition.circom

UniRep state transition proof between epochs

shielded_fee.circom

Privacy-preserving fee proofs (hides exact poll activation cost)

Zero-Knowledge Proof Flow

1

Identity Creation

User generates a Semaphore v4 identity (secret + commitment). On mobile the identity is Poseidon2-derived and persisted in SecureStore. The secret never leaves the client.

Identity = (secret, nullifier, commitment)
2

Group Registration

Identity commitment is added to the target group's Merkle tree (poll, community, or global). The backend tracks tree state; the client fetches updated witnesses for proof generation.

Merkle Tree = [commitment₁, commitment₂, ..., commitmentₙ]
3

Proof Generation

The client generates a ZK proof bundling: Semaphore membership, RLN nullifier (for anti-spam), optional UniRep reputation, and an action-specific statement (vote, post, comment, boost).

Proof = ZK(identity ∈ tree ∧ action_valid ∧ identity_hidden ∧ rate_limit_ok)
4

On-Chain / Off-Chain Verification

Groth16Verifier.sol verifies proofs on-chain for votes and governance actions. High-throughput paths (posts, reactions) route through the Gnark prover cluster and are anchored via ZKRollupDA batches.

verify(proof) → record(action) ∧ check(nullifier)

Core Packages — 22 Extracted @agora/* Modules

During v6.0–v8.0 the Agora monorepo extracted 22 independent packages into packages/core and packages/infra. Each package is versioned, unit-tested, and reusable across web, mobile, and backend.

@agora/auth

JWT, SIWE, OAuth, password, RBAC guards

@agora/identity

ZK identity, password hashing, RBAC helpers

@agora/identity-vault

Secure credential storage for mobile

@agora/schemas

7 shared domain schemas (auth/posts/polls/votes/messages/communities/wallet)

@agora/crypto

Encryption, key derivation, hashing, SIWE verification

@agora/zk-engine

Semaphore v4 integration, snarkjs, circuit loader

@agora/anon-credential

Anonymous credential system with eligibility checks

@agora/anon-receipt

Anonymous claim receipts for audit trails

@agora/token-engine

Wallet, rewards, staking, vesting, governance, treasury, batch payout

@agora/blockchain

Ethereum provider, contract ABIs, ethers 6.15 integration

@agora/storage

TypeORM entities, repositories, database abstraction

@agora/messaging

Real-time messaging infrastructure over Socket.IO

@agora/notifications

Email/SMS notification service

@agora/analytics

Event analytics, schema registry, k-anonymity aggregates

@agora/achievements

Achievement definitions and tracking

@agora/reputation

UniRep reputation protocol integration

@agora/subscriptions

Subscription tier management (Starter / Pro / Enterprise)

@agora/recommendations

Collaborative filtering recommendations

@agora/followers

Follow relationship management

@agora/realtime

Socket.IO event bus, backpressure handling

@agora/canton

Canton validator integration for private enterprise settlement

@agora/sms

SMS delivery (Twilio / Vonage)

ZK Rollup & Canton Infrastructure

ZK Rollup Stack

  • Gnark Prover (Go, gRPC:50051, 8 CPU / 32 GB RAM)
  • Prover worker pool (2 replicas, 2 CPU / 8 GB each)
  • Redis for job coordination (2 GB max memory)
  • Batch size 50, interval 30 s, max wait 300 s
  • Anchors via ZKRollupDA.sol state commitments
  • Prometheus + Grafana monitoring (port 3001)

Canton Validator

  • Private settlement layer for enterprise customers
  • Canton Coin (CC) flows settle off the public chain, visible only to the counterparties
  • HSM-backed key management for validator signing
  • Integrated via the @agora/canton package

ZK Canton Wallet — Private Spending

How your identity stays hidden even while spending

Voting is only half the story. When users spend Canton Coin to activate polls, unlock anonymous posts, or claim rewards, a naive wallet would expose their on-chain history and link every action back to a single address. Agora routes every balance through a Poseidon2 shielded pool — funds enter as a commitment, spend via a zero-knowledge proof of ownership, and re-emerge under a brand-new note. Observers see only the pool; the graph of who-pays-whom never leaves the user's device.

Sender
Wallet A
Holds notesecret_A
publish nullifier
Pool
Commitments
Merkle tree
fresh commitment
Recipient
Wallet B
New notesecret_B
Observer sees
Pool activity only
Cannot determine
A → B link, amount, balance
Validator confirms
Proof ✓ · Nullifier unused ✓
1

Deposit with shielded commitment

When funds enter the wallet, the user commits a Poseidon2 hash of (amount, secret, nullifier) to the pool. The public chain sees only the commitment — never the amount or the owner.

commitment = Poseidon2(amount, secret, nullifier)
2

Sender proves membership in-circuit

To spend, the user generates a Groth16 proof showing the commitment exists in the pool's Merkle tree and that they know its secret — without revealing which leaf is theirs. Canton parties, CC balances, and ledger observers learn nothing linkable.

ZK(commitment ∈ pool ∧ owner_knows_secret)
3

Nullifier prevents double-spend

A unique nullifier derived from the secret is published atomically with the spend. The Canton validator records it; the same commitment can never be spent twice. The nullifier is unlinkable to the original deposit for any outside observer.

nullifier = Poseidon2(secret, commitment_index)
4

Recipient receives a fresh shielded note

Funds re-enter the pool as a new commitment belonging to the recipient. Continuous pool in → pool out flow means watch-list addresses, balance analysis, and transaction-graph de-anonymization all break: every transaction looks like noise from outside.

new_commitment = Poseidon2(amount_out, recipient_secret, fresh_nullifier)

What this buys you

No address reuse. Each transaction mints a fresh commitment — no long-lived address to fingerprint.
Amount privacy. Poll activation costs, reward payouts, and CC transfers never leak the exact figure.
Unlinkable spending. Chain analysis cannot connect a deposit to a later spend, even with full ledger access.
Still verifiable. The validator (and anyone) can check proofs and nullifiers — integrity survives full anonymity.

Data Flow Architecture

1. Content Creation (Poll / Post / Community)

Organization or user creates content → Backend stores configuration → PostgreSQL + event store → Smart contract initialized with parameters (for polls, burn fee processed)

2. Identity Registration

User generates Semaphore identity (client-side, Poseidon2) → Commitment sent to backend → Added to target Merkle tree → Tree root updated → Mobile client persists identity in SecureStore

3. Anonymous Action (Vote / Post / Boost)

Client generates ZK proof (Semaphore + RLN + UniRep if applicable) → Proof + action sent to backend → Nullifier check → On-chain verification (Groth16Verifier) OR batched to Gnark prover cluster → Action recorded

4. Aggregation & Analytics

Backend aggregates actions from blockchain and event store → k-anonymity engine (k ≥ 25) produces privacy-preserving metrics → Community health, social health, and funnel dashboards → Results publicly verifiable

5. Rewards & Settlement

Reward calculation = Base × Risk Multiplier × Scale Multiplier → Merkle root published per epoch → AGRBatchPayout.sol allows gas-efficient claims → Unclaimed rewards expire after 90 days → Treasury health model adjusts next epoch

Merkle Trees for Group Membership

Agora uses Merkle trees to efficiently manage groups of eligible voters and community members without storing individual identities.

How It Works:

  • Each voter's identity commitment is a leaf
  • The tree root represents the entire eligible group
  • ZK proofs demonstrate membership without revealing which leaf
  • Tree depth 20 supports up to 1,048,576 members per group

Benefits:

  • Logarithmic proof size (efficient verification)
  • Scales to large voter groups
  • No need to store full member list on-chain
  • Dynamic updates via semaphore_batch circuit