A protocol recommendation for docs.ipe.city. Ten primitives, picked deliberately, integrated end-to-end, and running on Sepolia today. This is the stack we think every network city should adopt — and the working proof that they fit together.
Ipê (and every village like it) needs the same plumbing: who is a member, how do we decide, how do we remember what happened. Each of those questions has dozens of plausible answers. This repo is our argument for which ten to standardize on, and a reference implementation you can read, run, and fork.
The 2020s produced two parallel experiments:
- Network states & pop-up cities — Próspera, Zuzalu, Cabin, Vitalia, Edge City, Aleph, Network School. Real residents, real charters, real coordination problems.
- Credible-neutral crypto primitives — FHE, account abstraction, ENS, EAS, Unlock, on-chain governance. Audited, public-good, ownerless.
The first group keeps reinventing the second group's plumbing — usually badly, usually with Google Forms, usually with the founders' wallet as the ledger-of-record.
If the next decade has a hundred network cities, they shouldn't each rebuild the ballot box. A shared protocol stack means:
- A resident's EAS attestations follow them from Ipê to the next city.
- An ENS subname is recognized everywhere, not just on one Discord.
- Confidential governance is a dependency you import, not a project you staff.
- Audits, bug-bounty surface, and tooling compound across the ecosystem instead of fragmenting per-city.
A network-city governance stack should answer eight questions, each at its own layer, each with one canonical primitive. Stacked from the user-facing top down to settlement at the bottom:
┌────────────────────────────────────────────────────────────────────────┐
│ L8 · Identity What is this person called? │
│ → ENS + NameWrapper │
├────────────────────────────────────────────────────────────────────────┤
│ L7 · Provenance What have they done? │
│ → EAS (IpeResident · IpeCheckin · IpeRole · IpeProject · │
│ IpeSkill) │
├────────────────────────────────────────────────────────────────────────┤
│ L6 · Confidentiality & Privacy How do their choices stay private? │
│ → Zama fhEVM (FHE-encrypted ballots, aggregate-only decrypt) │
├────────────────────────────────────────────────────────────────────────┤
│ L5 · Governance How do decisions move? │
│ → OpenZeppelin Governor + Liquid Delegation │
├────────────────────────────────────────────────────────────────────────┤
│ L4 · Membership Who is allowed? │
│ → Unlock Protocol (non-transferable ERC-721 keys) │
├────────────────────────────────────────────────────────────────────────┤
│ L3 · Access How do they transact? │
│ → Privy + ERC-4337 + EIP-7702 (Pimlico) │
├────────────────────────────────────────────────────────────────────────┤
│ L2 · Content Where do long documents live? │
│ → IPFS (Pinata) │
├────────────────────────────────────────────────────────────────────────┤
│ L1 · Settlement Where is state final? │
│ → Ethereum (Sepolia pilot → mainnet or L2 in production) │
└────────────────────────────────────────────────────────────────────────┘
Each layer can be adopted independently. Each layer has exactly one recommendation. No bespoke cryptography, no vendor lock-in beyond swappable hosted providers (Pinata, Pimlico, Privy).
| Layer | Pick | Why this and not the alternative |
|---|---|---|
| L8 · Identity | ENS + NameWrapper | A name, on L1, that travels with the holder. Beats Discord handles and SBTs that nobody renders. |
| L7 · Provenance | EAS | Schema'd, revocable, queryable, portable across cities. Beats bespoke databases and Notion exports. |
| L6 · Confidentiality & Privacy | Zama fhEVM | True FHE — ballots stay sealed end-to-end, only the aggregate is ever decrypted. Beats commit-reveal (coercible) and ZK ballots (heavier client cost, harder UX). |
| L5 · Governance | OpenZeppelin Governor + Liquid Delegation | The most-audited governance contract in production, plus per-proposal liquid delegation. Beats roll-your-own, Tally-only flows, and sticky ERC-20 delegation. |
| L4 · Membership | Unlock Protocol | Non-transferable ERC-721 keys. Beats Snapshot allowlists (off-chain, mutable) and ERC-20 voting (speculation, whales). |
| L3 · Access | Privy + ERC-4337 + EIP-7702 | Email/social → embedded wallet → sponsored gas, gated by membership. Beats walletconnect-only and "we'll airdrop gas". |
| L2 · Content | IPFS (Pinata) | The ledger keeps the hash; the network keeps the bytes. Beats centralized blob stores. |
| L1 · Settlement | Ethereum | Sepolia for the pilot; mainnet or your favorite L2 once the institution is permanent. |
- A vote is L4 → L5 → L6 → L2 → L1: an Unlock-key holder (L4) calls the Governor (L5) with an FHE-encrypted ballot (L6); the proposal body lives on IPFS (L2); the result settles on Ethereum (L1).
- Joining a city is L3 → L4 → L8 → L7: Privy onboards the user with sponsored gas (L3); they claim an Unlock key (L4); they wrap an ENS subname (L8); their residency is recorded as an EAS attestation (L7).
- A skill record is L7 alone: an EAS
IpeSkillattestation is portable across cities even if the holder leaves Ipê — provenance survives membership.
If docs.ipe.city takes this stack as canonical, the table of contents writes itself — one section per layer:
| Docs section | Layer | Points at |
|---|---|---|
| Identity | L8 | ENS subnames under a city-owned parent → "claim your *.ipecity.eth" |
| Provenance | L7 | The five EAS schemas in @ipe-gov/eas → "your role history is portable, here's the SDK" |
| Voting | L6 + L5 | This contract set → "ballots are FHE-encrypted, here's how the math works, here's the audit" |
| Membership | L4 | Unlock → "your residency is an Unlock key, here's how to claim one" |
| Onboarding | L3 | Privy + sponsored gas → "no ETH required, email is enough" |
| Content & storage | L2 | IPFS envelope format → "this is how a proposal body is shaped" |
| Chain | L1 | Sepolia today, mainnet/L2 tomorrow |
Every doc page becomes a thin pointer to a working primitive at a known
layer, instead of a spec to be reimplemented. The five EAS schemas in
@ipe-gov/eas are already the right granularity for the city's identity
surface — name them, register them under the city's address, and the docs
write themselves around them.
You don't have to take all eight layers at once. Each is independently useful; the higher layers depend only on the lower ones being present.
| Adoption path | Layers | What you get |
|---|---|---|
| Identity-only | L1 + L2 + L7 + L8 | Named members + portable EAS attestations. No on-chain voting yet. Use @ipe-gov/eas + apps/ens-api. |
| + Membership | add L4 | Now attestations have a real residency predicate. Still no governance. |
| + Access | add L3 | Members onboard with email and transact without holding ETH. |
| Full governance | add L5 + L6 | Deploy UnlockConfidentialGovernorLiquid against your Lock; register schemas; run the three workers. The whole institution. |
| Production migration | swap L1 | Sepolia → mainnet (or your L2). Contracts, schemas, and worker abstractions don't change — update the address book in @ipe-gov/sdk; ship. |
- Public-good primitives over bespoke cryptography. Every protocol in the stack is audited, open-source, and used in production by someone else.
- Off-chain hops verify membership. Workers are gates, not trust roots.
Every privileged action checks
PublicLock.getHasValidKeybefore doing work. - One source of truth. Addresses, ABIs, schema UIDs live in
@ipe-gov/sdkand@ipe-gov/eas. Apps and workers import; they never inline. - The institution should outlive the pilot. Sepolia is a learning environment. The contracts, schemas, and key abstractions are designed to port to mainnet (or an L2) — and to other cities — without rewriting the village.
Live on Sepolia. Ipê is the first instance. Recommending this stack to docs.ipe.city and any network city that wants confidential governance, portable identity, and an onboarding flow that doesn't gatekeep.
"A village runs on what its members can keep to themselves — and what they choose to count together." — Charter, §1
The pitch above is concrete because every layer is already shipped. This section is the lay of the land: the SDKs you can import, the public services the community already runs, and how the monorepo is organized.
A working pilot of the full stack on Sepolia, plus the SDKs needed to integrate any of it independently:
@ipe-gov/contracts—UnlockConfidentialGovernorLiquid(Governor variant wired to Unlock as the vote source) andLiquidDelegation. Drop in your Lock address; you have a confidential governance system.@ipe-gov/eas— Five Ipê EAS schemas with codecs and Sepolia registration scripts. Rename them to your city; keep the shape.@ipe-gov/sdk— Single source of truth for addresses and ABIs.@ipe-gov/ipfs— Shared envelope format and Pinata helpers.@ipe-gov/workers-shared— Hono middleware (CORS, signature verification, error shapes) for the worker pattern.apps/web— Reference frontend (Vite + TanStack Router + Privy + wagmi). Encrypts ballots client-side with the FHEVM SDK.apps/starter— Minimal fork-ready dApp template on the same stack asapps/web. Blank canvas with Privy + wagmi + the Sepolia membership gate pre-wired. Clone the whole monorepo to get it alongside every reference app and shared package —git clone https://github.com/MihkelJ/ipe-gov.git.apps/pin-api— Worker that signature-gates IPFS pinning.apps/paymaster-proxy— Worker that gates ERC-4337 sponsorship by Unlock key + chain-aware RPC proxy.apps/ens-api— Worker that issues wrapped ENS subnames on L1.
The pilot is live. These are public, community-run endpoints — any network-city app, bot, or integration can use them today without spinning up its own infrastructure. They share the same gating rules: every privileged call verifies the caller holds a valid Unlock key on Sepolia.
| Service | URL | What it does |
|---|---|---|
| Web app (L8 → L1) | Cloudflare Pages — ipe-gov-web |
The reference frontend. Encrypts ballots client-side, talks to the workers below. |
| pin-api (L2) | https://ipe-gov-pin-api.ipe-gov.workers.dev |
Signature-gated IPFS pinning for proposal bodies. Sign a timestamped message; get a CID back. |
| paymaster-proxy (L3) | https://ipe-gov-paymaster-proxy.ipe-gov.workers.dev/rpc |
ERC-4337 / ERC-7677 endpoint that gates Pimlico sponsorship by Unlock-key holdership. Doubles as a chain-aware RPC proxy. |
| ens-api (L8) | https://ipe-gov-ens-api.ipe-gov.workers.dev |
Issues wrapped ENS subnames under govdemo.eth on L1 mainnet. |
| Sepolia contracts (L5 + L6) | Addresses in @ipe-gov/sdk |
UnlockConfidentialGovernorLiquid, LiquidDelegation, the Unlock PublicLock. |
| EAS schemas (L7) | UIDs in @ipe-gov/eas (registered on Sepolia) |
IpeResident, IpeCheckin, IpeRole, IpeProjectLaunched, IpeSkill. |
If you're integrating from another app, you only need @ipe-gov/sdk +
@ipe-gov/eas — both packages re-export the live addresses, ABIs, and
schema UIDs so the public services above are a npm install away.
ipe-gov/
├── apps/
│ ├── web/ # Vite + TanStack Router frontend (Cloudflare Pages)
│ ├── starter/ # Minimal fork-ready dApp template on the same stack
│ ├── pin-api/ # Worker: signature-gated IPFS pinning
│ ├── paymaster-proxy/ # Worker: ERC-7677 paymaster + chain-aware RPC proxy
│ └── ens-api/ # Worker: wrapped ENS subname issuance
├── packages/
│ ├── contracts/ # Hardhat + FHEVM governor, liquid delegation, deploy scripts
│ ├── sdk/ # Shared addresses, ABIs, helpers (single source of truth)
│ ├── eas/ # EAS schema definitions, codecs, Sepolia registration
│ ├── ipfs/ # IPFS pin/fetch helpers (web + pin-api)
│ └── workers-shared/ # Hono middleware + helpers shared across workers
├── pnpm-workspace.yaml
├── turbo.json
└── package.json
- contracts —
UnlockConfidentialGovernorLiquidchecks the caller holds a valid Unlock key, stores proposal metadata (IPFS CID + target calls + per-proposal voting window), and accumulates encrypted vote tallies aseuint32via FHEVM.LiquidDelegationlets members delegate per proposal — the delegation graph is public; individual votes stay encrypted. - sdk — Re-exports compiled ABIs, deployed addresses, and shared helpers
(e.g.
paymasterProxyRpcUrl). Never inline addresses or ABIs in app or worker code — import from@ipe-gov/sdk. - eas — Source of truth for the Ipê EAS schemas plus Hardhat scripts to
register them on Sepolia. Apps consume schema UIDs and codecs from
@ipe-gov/eas. - pin-api — Browser can't hold a Pinata JWT. Client signs a timestamped message; worker verifies signature + Unlock key, pins the JSON, returns the CID. The CID goes on-chain with the proposal.
- paymaster-proxy — ERC-4337 / ERC-7677 JSON-RPC endpoint. Verifies
userOp.senderholds a valid Unlock key before forwarding to Pimlico. Doubles as a chain-aware RPC proxy used by other workers and the web app. - ens-api — Issues wrapped ENS subnames on L1. Holds an operator key
with
setApprovalForAllon the parent name and records each claim in a KV namespace. Mainnet RPC routes throughpaymaster-proxy. - workers-shared — Hono middleware (CORS, signature verification, error shapes). Workers import from here; workers do not re-export library code to other workers — share helpers via this package.
- web — Vite + TanStack Router with Privy + wagmi. Talks to
VITE_PIN_API_URL,VITE_PAYMASTER_PROXY_URL, andVITE_ENS_API_URL. - ipfs — Shared envelope format (
version,kind,proposer,createdAt,text) plus Pinata pinning and gateway fetch helpers.
apps/web is a reference implementation, not the product. It's there to
prove the eight-layer stack composes end-to-end and to give cities a working
template they can fork or replace. Everything below describes that specific
app — its UX flow, its wiring, and how to run or deploy it.
01 YOU CHOOSE Yes / No / Abstain, locally [plain]
02 WE SEAL Encrypted in your browser [sealed]
03 CHAIN SUMS Ciphertexts add without opening [sealed]
04 TOTAL REVEALED Only the aggregate is decrypted [plain]
Steps 02 and 03 are mathematically opaque, by construction. The vote happened; your choice did not leak. Holds whether your city has 30 residents or 30,000.
┌──────────────────────────────────────────────┐
│ apps/web (Cloudflare Pages) │
│ Vite · TanStack Router · Privy · wagmi │
│ Encrypts ballots client-side with FHEVM SDK │
└──────┬─────────────┬───────────────┬─────────┘
│ │ │
┌─────────────▼──┐ ┌───────▼──────┐ ┌─────▼──────────┐
│ pin-api │ │ paymaster- │ │ ens-api │
│ Worker │ │ proxy Worker │ │ Worker (L1) │
│ Sig-gated │ │ ERC-7677 + │ │ Wraps subnames │
│ IPFS pinning │ │ RPC proxy │ │ under govdemo │
└────────┬───────┘ └──────┬───────┘ └────────┬───────┘
│ CID │ UserOp / RPC │ NameWrapper
▼ ▼ ▼
┌────────────────────────────────────────────────────┐
│ Ethereum Sepolia (pilot) · L1 mainnet (ENS only) │
│ │
│ UnlockConfidentialGovernorLiquid (FHEVM) │
│ LiquidDelegation │
│ PublicLock (Unlock) EAS schemas (5) │
└────────────────────────────────────────────────────┘
Pinata, Pimlico, and the operator key for ENS issuance never touch the
browser. Every privileged action checks PublicLock.getHasValidKey before
doing work — workers are gates, not trust roots.
- Node.js ≥ 20
- pnpm ≥ 10
pnpm installEach app has a template; copy and fill in:
cp apps/web/.env.example apps/web/.env.local
cp apps/pin-api/.dev.vars.example apps/pin-api/.dev.vars
cp apps/paymaster-proxy/.dev.vars.example apps/paymaster-proxy/.dev.vars
cp apps/ens-api/.dev.vars.example apps/ens-api/.dev.vars
cp packages/contracts/.env.example packages/contracts/.envHardhat prefers its vault over .env:
cd packages/contracts
npx hardhat vars set MNEMONIC
npx hardhat vars set INFURA_API_KEY
npx hardhat vars set ETHERSCAN_API_KEY # optional, for verificationWorker secrets — production values for every Cloudflare Worker
(sensitive or not) go through wrangler secret put <NAME>, never vars in
wrangler.jsonc. Local-dev secrets live in each worker's .dev.vars
(gitignored).
pnpm dev # run every package's dev task in parallel
pnpm build # build every package
pnpm compile # compile contracts + export ABIs to the sdk
pnpm test # run contract tests
pnpm deploy:sepolia # deploy contracts to SepoliaScoped to one package:
pnpm --filter @ipe-gov/web run dev
pnpm --filter @ipe-gov/pin-api run dev
pnpm --filter @ipe-gov/paymaster-proxy run dev
pnpm --filter @ipe-gov/ens-api run dev
pnpm --filter @ipe-gov/contracts run compile
pnpm --filter @ipe-gov/eas run register-schemas:sepoliaAlways scope worker deploys to a single app — never run wrangler deploy
from an ambiguous cwd:
pnpm --filter @ipe-gov/pin-api run deploy
pnpm --filter @ipe-gov/paymaster-proxy run deploy
pnpm --filter @ipe-gov/ens-api run deploy| Package | Target |
|---|---|
apps/web |
Cloudflare Pages |
apps/pin-api |
Cloudflare Workers |
apps/paymaster-proxy |
Cloudflare Workers |
apps/ens-api |
Cloudflare Workers |
packages/contracts |
Sepolia (Hardhat) |
packages/eas |
Sepolia (Hardhat — schema registry) |
CI/CD is not yet wired up.