Back to Whitepaper
SECTION 03

Technical Architecture

Deep dive into our system design and technology stack

Three-Layer Architecture at Scale

The anonymous lane has its own front door. Alongside the authenticated API, an anonymous action is posted to a separate endpoint, POST /v1/anonymous-actions, which rejects — rather than strips — any request carrying an authorization header, a cookie, a trace header, an API key, or a user, session or device header, so a client that would have leaked identity gets a 400 instead of a quiet success; the reads that answer questions about anonymous artefacts, such as membership manifests, sit behind the same rule. What the endpoint does not remove is the network: the request reaches the gateway over plain HTTPS and the gateway sees the client address, so an authenticated session and an anonymous submission from the same address seconds apart remain joinable by the operator, and while an authenticated WebSocket is open from that address the join is a lookup rather than an inference. Closing that needs a third-party relay we do not run. This lane has served production since 28 August 2026 for anonymous ballots; the earlier v1 write routes for community posts, comments and reactions were withdrawn at the cutover and answer 410 Gone. The layers are connected by an event-driven 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 52 modules and 28 extracted @agora/* packages. PostgreSQL, Redis, Bull Queue, Socket.IO, analytics engine with k-anonymity.
  • Settlement: Agora's own Canton mainnet validator, with a DAML package of write-only records co-signed, since 2 September 2026, by Agora's app-provider party. Canton Coin settlement plus a shielded fee pool that hides which payer funded a fee, not the amount. Ledger recording is on in production and has carried real records; the fee pool holds deposits and, as of 2 September 2026, had not yet paid a fee.

Agora operates its own Canton mainnet validator. Poll events, vote records and token operations are modelled as write-only entries in a DAML package, co-signed since 2 September 2026 by Agora's app-provider party; that recording sits behind a configuration switch which is on in production, and the ledger has carried vote records, poll-created events and social records. Canton Coin settles fees through a shielded pool that separates a deposit from the spend it later pays for, so a fee does not identify the payer who funded it — the fee amounts themselves are public. The backend exposes a Prometheus scrape endpoint; the Grafana dashboards that would consume it are configuration in this repo, not a deployed stack.

Frontend Layer

Proofs are generated on the client. On web, the v2 path derives the voting identity in the browser and proves in a Web Worker, so the identity secret stays on the device; that path has been live in production since 28 August 2026. Shipped mobile builds still run the older flow, in which the server mints the voting identity and sends it to the app; the mobile v2 client is in progress. 122 web routes, 20 mobile feature modules.

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

Key Responsibilities:

  • •Client-side proof generation (Semaphore v4 Groth16 membership proofs, blind-signature credential minting, shielded-fee spend proofs)
  • •Global + per-community ZK identity (SecureStore + Poseidon on mobile)
  • •WebView-based Groth16 proving on mobile for the v1 Semaphore flow, until the mobile v2 client ships
  • •Offline quota tracking, real-time updates via Socket.IO
  • •Three theme variants: Modern neon, Classic Light, Classic Dark

Backend Layer

NestJS monorepo with 52 modules and 25 extracted core packages. Runs the API, orchestrates ZK proof batching, drives analytics, and anchors settlement state on the Canton ledger.

NestJS 11TypeScriptPostgreSQLRedisTypeORMBull QueueSocket.IO

Key Responsibilities:

  • •52 domain modules across polls, votes, posts, communities, messaging, moderation, analytics, anonymous-action issuance and Canton settlement
  • •28 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

Canton Settlement Layer

Agora runs its own live Canton Network mainnet validator. A DAML attribution ledger records token operations, vote receipts, and poll-created events, while Canton Coin settlement handles poll-activation fees and rewards privately.

DAMLCanton NetworkSplice validatorCanton CoinPoseidonHSM key mgmt

Key Responsibilities:

  • •DAML attribution ledger: token operations, vote receipts (BatchAnchored / VoteRecorded), poll-created events
  • •Vote receipts and poll lifecycle events anchored on the Canton ledger
  • •Canton Coin settlement for poll-activation fees and rewards
  • •Shielded fee pool (Poseidon commitments in a fixed depth-24 Merkle tree) separates a deposit from the spend it funds, so a fee spend does not name the depositor; the fee amount is a public signal checked against a public price table
  • •Featured App status is not held, so no app rewards accrue today — the reward rules changed under CIP-0104 and CIP-0116

Anonymous-Action Stack — Blind-Signature Credential + Semaphore v4 + Cohort Attestation

Agora's anonymous lane combines four mechanisms, only one of which is a zero-knowledge membership proof. A Privacy Pass publicly-verifiable token — an RFC 9474 RSA blind signature, profile rfc9578-blind-rsa-2048-sha384-pss-48 — authorises the action; it is one-time and unlinkable to its issuance. A Semaphore v4 Groth16 proof over a depth-20 Merkle tree establishes membership without naming the member. An Ed25519 cohort attestation reports which side of the readiness threshold the issuance cohort sits on, as a bucket rather than a count. On the charging lane, a Groth16 circom proof spends a deposit from the shielded fee pool. A ten-field keccak256 request digest binds the credential, the membership proof and the fee proof into one action, so a proof from one submission is refused on another. RLN is a v1 mechanism and v2 does not inherit it: in v1 the identity commitment sat beside the user id, which left an RLN-gated post attributable to its author by the operator; the v1 community routes were withdrawn from production at the cutover. The UniRep-inspired reputation layer likewise sits outside this lane, and its circuits are written but not compiled. The lane has served production since 28 August 2026 for anonymous ballots on the web client.

semaphore_batch.circom

Batch Semaphore proofs — multiple votes in a single transaction

rln.circom

Rate-limiting nullifier — the v1 lane's spam limiter, retired from the live routes; kept as the record of the previous design

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

Fee spend proofs (hide which deposit paid a fee; the fee is public)

Zero-Knowledge Proof Flow

1

Identity Creation

User generates a Semaphore v4 identity (secret + commitment). On mobile the identity is Poseidon-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 assembles one envelope: a blind-signed anonymous credential, a Semaphore v4 membership proof, and — only where the action carries a fee — a shielded-pool spend proof. A single request digest, computed over the canonical action, the policy, and the credential's spend nullifier, is folded into the membership proof's message and into the fee proof's public inputs, so a proof minted for one action cannot be replayed onto another.

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

Verification & Canton Anchoring

The server verifies the envelope, records the action, and — since August 2026 — stores the envelope itself in the same transaction, refusing it unless it hashes to the digest the action committed to. Where Canton settlement is switched on, the organization's own Canton party writes the vote record (for an organization without one, Agora's system party records the vote and Agora's provider party records the poll and its evidence anchor), deferred until a sealed poll's results unseal. Ledger recording is on in production, and the write path has carried real vote records; since 2 September 2026 each carries two signatories.

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

Core Packages — 22 Extracted @agora/* Modules

The Agora monorepo factors shared logic into 28 versioned @agora/* packages: 25 domain and cryptography packages under packages/core, and 3 interface packages under packages/ui. Each app pulls in only what it needs — the backend depends on 24 of them, the web app on 10, the mobile app on 8 — with @agora/schemas, @agora/crypto and @agora/zk-engine shared by all three.

@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

Canton ledger client — DAML contract bindings and settlement helpers

@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)

Canton Settlement 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
  • •Batches anchored on the Canton ledger
  • •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 Balance — Fees Without a Payer Address

How a fee is paid without naming the payer on the ledger

The in-app balance exists for one purpose: paying the small fees that keep the platform running. A naive account would expose that payment on the ledger and link every fee back to a single address. Agora routes an anonymous action's fee through a Poseidon shielded pool — the deposit enters as a commitment and is spent with a zero-knowledge proof of ownership. Observers see the pool rather than a payer address. The fee amount is not hidden: it is a public input to the spend proof, checked against the price quoted for that action. What the pool hides is which deposit funded it.

Payer
Your balance
Holds commitmentsecret
→prove + nullifier
Pool
Commitments Merkle tree
→fee settled
Platform fee
Anonymous action
Spent from poolno payer address
Observer sees
Pool activity and the fee amount
Not revealed
Which deposit funded it
Agora's backend confirms
Proof ✓ · Nullifier unused ✓
1

Commit the deposit privately

When a user funds the pool, the client sends an inner commitment and the server hashes the charged denomination into the leaf. The Canton ledger sees a commitment rather than a payer address. The depositing party is still visible in the CC transfer log — depositing is not the anonymous step, spending is.

commitment = Poseidon(Poseidon(secret, nonce), denomination)
2

Prove the fee in-circuit

To pay an anonymous action's fee, the user generates a Groth16 proof that their commitment sits in the pool's Merkle tree and that they know its secret, without revealing which leaf is theirs. The proof publishes the fee, the tree root and a nullifier; the deposit's denomination stays in-circuit, proved only to cover the fee.

ZK(commitment ∈ pool ∧ knows secret ∧ denomination ≥ fee)
3

Nullifier prevents double-payment

A nullifier derived from the secret is published with the spend. Agora's backend records it under a unique index and publishes it in the public spent log, so the same deposit cannot be spent twice. The nullifier is unlinkable to its commitment for any observer that does not hold the identified deposit table — the operator holds it.

nullifier = nonce = Poseidon(secret, deposit_index)
4

Fee settles out of the pool

The fee settles from the pool rather than from a visible address, so transaction-graph analysis has no payer to follow. The anonymity set is the unspent deposits whose denomination covers the fee.

verify(proof) ∧ nullifier ∉ spent → fee paid from pool

What this buys you

✓No address reuse. Each fee is spent from a fresh commitment — there is no long-lived address to fingerprint.
✓Denomination privacy. The fee itself is public. The size of the deposit it came from is not — the circuit proves only that the deposit covers the fee.
✓Unlinkable on the ledger. Ledger analysis does not name the payer of a fee. The operator's own deposit table does; see the threat model for that boundary.
✓Still verifiable. Agora's backend checks every proof, and the spent nullifiers are published for anyone to read — integrity does not depend on knowing who paid.

Data Flow Architecture

1. Content Creation (Poll / Post / Community)

Organization or user creates content → backend stores configuration → PostgreSQL + event store → on a deployment running Canton settlement, activating a poll transfers any non-zero activation fee in Canton Coin from the organisation's Canton party to the treasury party; writing the poll-created record to the Canton ledger is a separate switch, on in production, and because the fee settles first a failed record leaves the poll active with no ledger entry

2. Identity Registration

User generates Semaphore identity (client-side, Poseidon) → 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 builds the canonical envelope and generates the Semaphore membership proof in a browser worker → POST /v1/anonymous-actions, an ingress that rejects the request outright — it does not strip it — when it carries an authorization header, a cookie, a trace header or any identity header → the credential is checked against the signed issuer key manifest, the cohort readiness gate is applied, and the membership proof is verified against the pinned verification key → one transaction inserts the spend nullifier and the semantic nullifier under unique constraints, records the action, and stores the canonical envelope as evidence — that insert, not the circuit, is what a second ballot in the same poll collides with → the ledger record follows afterwards, written to Canton by the organisation's own party, deferred while a sealed poll is open, never inline with the request

4. Aggregation & Analytics

Backend aggregates actions from the Canton ledger and event store → k-anonymity engine (k ≥ 25) produces privacy-preserving metrics for the cohort dashboards → Community health, social health, and funnel dashboards → Results anchored on the ledger, where the organisation that ran the poll can check them against the record

5. Rewards & Settlement

Reward calculation runs off-chain → a claimed v2 participation receipt credits a UTC activity day rather than a poll → reward rows carry no poll link, and the points ledger refuses a participation row that references anything but the day → the line this draws: registering says "I may vote" and stays visible to the organiser by design, while being rewarded says "I voted" and is recorded only to the day

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 the full member list on the ledger
  • Registrations and revocations produce a new snapshot of the tree, and the client proves against the published root