---
title: dao.new specification
description: Product and technical specification for dao.new, a one-call service that deploys a Gov DAO and hands out claim codes for membership, built on Gov, Wulong and Genji.
date: 2026-10-13
lang: en-US
author: Julien Béranger
model: Claude Opus 5.5
conversation: https://claude.ai/chat/cede29d7-5b8d-4438-8d67-ae96ceb509d7
source: https://julienberanger.com/dao-new-specification
---

# dao.new specification

## Summary

dao.new lets any online service give real decision power to its community in one API call. The service sends either a list of email addresses or a number of members. The relayer deploys a [Gov](https://github.com/w3hc/gov) DAO and returns its address with one claim code per member. Each code claims one non-transferable membership NFT, which carries one vote.

The service then sends part of its budget (1%, 5% or more, there is no cap) to the DAO. From that point the money is controlled by the community's votes, not by the service. The DAO can also vote on the state of a piece of data: a content hash, a website build hash, a manifesto.

Members need no crypto knowledge and no ETH. They sign in with a [passkey](https://en.wikipedia.org/wiki/WebAuthn) through [w3pk](https://w3pk.w3hc.org/), and the relayer pays gas for them. The relayer can never act in a member's name: every action is a signature only the member can produce, so the relayer is dispensable.

## Use case

A legacy online service (a forum, a media site, an open-source project, a cooperative platform) wants to share governance without migrating its users to a crypto product.

1. The service creates a DAO on dao.new with its community's emails, or a member count if it distributes codes itself.
2. Members claim their membership from the email link.
3. The service funds the DAO's treasury.
4. Members propose and vote on spending, and on the state of data the service commits to respect (for example: "the production site must match build hash X").

Because the result is on-chain, the service's commitment becomes checkable by anyone, not only by the community.

## Components

| Component | Role | Status |
| --- | --- | --- |
| [Gov](https://github.com/w3hc/gov) | Governor and membership NFT, based on [OpenZeppelin Governor](https://docs.openzeppelin.com/contracts/5.x/api/governance) | Exists; one extension needed |
| `DaoFactory` | Deploys and wires one DAO per call, atomically | New |
| `ClaimOperator` | Gov's operator; mints for each valid claim code once, and for the service's admin key within a capped rate | New |
| Admin helper | A funded key and a small script the service uses to add members who join later | New |
| `StateRegistry` | Key-to-hash store that only the DAO can write | New |
| [Wulong](https://github.com/w3hc/wulong) | Relayer API running in a [TEE](https://en.wikipedia.org/wiki/Trusted_execution_environment) on [Phala Cloud](https://phala.network/) | Exists; relayer endpoints needed |
| [Genji](https://github.com/w3hc/genji) | [Next.js](https://nextjs.org/) UI with passkey wallets and WCAG 2.1 AA accessibility | Exists; dao.new pages needed |

Default chain: [Base](https://base.org/), which Wulong already uses for anchoring. [Crosschain Gov](https://github.com/w3hc/gov-crosschain) is a later option for communities on other chains.

## On-chain design

### What Gov already provides

Gov's `NFT.sol` and `Gov.sol` cover most requirements as they are:

- **One member, one vote.** `_mint` reverts with `OneMemberOneVote` if the address already holds a membership.
- **Non-transferable.** `_update` reverts with `NFTNonTransferable` on any transfer; only mints and burns pass.
- **Automatic self-delegation.** Every mint path calls `_delegate(to, to)`, so a new member's vote counts without any action from them.
- **Operator role.** `operatorMint(to, uri)` is callable by `operator` until `operatorExpiration`. Both are set by the NFT's owner (the Governor).
- **Governance-controlled membership.** `safeMint`, `govBurn` and `setMetadata` are owner-only, so after the claim window only votes can add or remove members.
- **Manifesto.** `setManifesto` stores an [IPFS](https://ipfs.tech/) CID, changeable only through governance.
- **Treasury.** There is no timelock: the Governor executes proposals itself, so the Governor's address is the treasury.
- **Timestamp clock.** `clock()` returns `block.timestamp`, so voting delay and period are in seconds.
- **Quorum.** `GovernorVotesQuorumFraction` sets quorum as a fraction of the NFT's total supply at the proposal snapshot. Unclaimed codes don't count toward it.

### DaoFactory

The NFT and the Governor reference each other: the Governor needs the NFT address in its constructor, and the NFT needs an owner that will be the Governor. The factory resolves this in one transaction.

```solidity
function createDao(DaoParams calldata p) external returns (address gov, address nft, address claim, address registry);
```

Sequence inside `createDao`:

1. Deploy `NFT` with `initialOwner = address(this)` and an empty `_firstMembers` list.
2. Deploy `Gov` with the NFT as its token and the parameters from `p`.
3. Deploy `ClaimOperator` with the NFT address, the Governor as owner, the codes' Merkle root, the tree's IPFS CID, the claim deadline, and the admin settings (admin address, cap, period, expiry).
4. Call `nft.setOperator(claim)` and `nft.setOperatorExpiration(max(p.claimDeadline, p.adminExpiry))`. The NFT has a single operator expiry, so it is set to the later of the two; `ClaimOperator` enforces each path's own deadline.
5. Deploy `StateRegistry` with `owner = gov`.
6. Call `nft.transferOwnership(gov)`.
7. Emit `DaoCreated(gov, nft, claim, registry, p.name)`.

Once `createDao` returns, the factory holds no power over the DAO. Anyone can call it, so the relayer is not required for deployment either.

Gov is deployed in full each time, not as an [ERC-1167](https://eips.ethereum.org/EIPS/eip-1167) minimal-proxy clone. On Base this costs little per DAO; clones can come later if volume justifies the work of making Gov initializable.

### ClaimOperator and claim codes

The claim flow has to resist [front-running](https://en.wikipedia.org/wiki/Front_running): if a claim transaction contained the code in plain text, anyone watching pending transactions could copy it and claim first. So a code is never revealed on-chain.

**Code generation (off-chain, by the relayer):**

1. For each member, draw 16 random bytes: the `code`.
2. Derive a private key: `codeKey = keccak256(abi.encodePacked("dao.new", chainId, factoryAddress, nonce, code))`, where `nonce` is the deploy's unique request ID. The code's address is `codeAddress = address(codeKey)`.
3. Build a [Merkle tree](https://en.wikipedia.org/wiki/Merkle_tree) whose leaves are the `codeAddress` values, using [OpenZeppelin's merkle-tree library](https://github.com/OpenZeppelin/merkle-tree).
4. Publish the tree (addresses only, nothing secret) to IPFS. Its CID is stored in `ClaimOperator`, so any client can rebuild a proof without the relayer.
5. Encode each `code` in [Base58](https://en.wikipedia.org/wiki/Base58) (about 22 characters) and return them, or email them as `https://dao.new/claim/<dao>#<code>`. The code sits in the [URL fragment](https://en.wikipedia.org/wiki/URI_fragment), which browsers never send to the server.

The derivation string ties every code to one DAO, so a code from one DAO is useless on another. 128 bits of randomness put brute-forcing the published addresses out of reach.

**Claiming (on-chain):**

```solidity
function claim(address recipient, address codeAddress, bytes32[] calldata proof, bytes calldata signature) external;
```

`claim` checks that:

- the claim deadline has not passed;
- `codeAddress` is a leaf of the stored root (`proof`);
- `codeAddress` has not been used;
- `signature` is a valid [EIP-712](https://eips.ethereum.org/EIPS/eip-712) signature by `codeAddress` over `Claim { address nft; address recipient; }`, with the chain ID in the domain.

It then marks the code used and calls `nft.operatorMint(recipient, tokenUri)`. The NFT itself rejects a recipient who already holds a membership.

Because the signature commits to `recipient`, a copied transaction can only ever mint to the intended member. Anyone may submit it, so a member with ETH never depends on the relayer.

**Why this neutralizes Gov's operator warning:** Gov's README warns that an operator can mint arbitrarily many memberships until expiry. Here the operator is a contract that can only mint for codes committed at deploy time, each once, until the claim deadline, plus the capped admin path below, which the DAO can shrink or shut off.

### Admin minting helper

Communities keep growing after the claim window. Without help, each new member would need a governance vote (`safeMint`). The admin helper lets the service add new users directly, within limits the DAO controls.

**What the service gets:** an admin key (an ordinary [Ethereum](https://ethereum.org/) private key, held by the service as a hot key) funded with a small amount of ETH for gas, and a script that mints a membership to a new user's address.

**Why it goes through `ClaimOperator`:** Gov's NFT has a single `operator` slot, already taken by `ClaimOperator`. So the admin path lives in the same contract, which lets the contract apply limits that a raw operator key would not have:

```solidity
address public admin;
uint256 public adminCap;        // mints allowed per period
uint256 public adminPeriod;     // in seconds
uint256 public adminExpiry;     // timestamp; 0 = no expiry

function adminMint(address to) external;                                  // called by the admin key itself
function adminMintBySig(address to, uint256 nonce, bytes calldata sig) external; // admin signs, relayer submits
function setAdmin(address newAdmin) external onlyOwner;                    // owner = Governor
function setAdminLimits(uint256 cap, uint256 period, uint256 expiry) external onlyOwner;
```

`adminMint` checks that the caller is `admin`, that `adminExpiry` has not passed, and that fewer than `adminCap` mints happened in the current period (`block.timestamp / adminPeriod`). It then calls `nft.operatorMint(to, tokenUri)`. `adminMintBySig` does the same from an [EIP-712](https://eips.ethereum.org/EIPS/eip-712) signature `AdminMint { address to; uint256 nonce; }`, so a service can keep the key unfunded and let the relayer pay gas.

**Joining flow:**

1. A new user signs up on the service.
2. The service sends them to `https://dao.new/join/<dao>`, where Genji creates a passkey wallet and returns its address to the service (redirect or callback URL).
3. The service runs the helper with that address.
4. The user's membership appears; they can vote on proposals whose snapshot falls after the mint.

**The helper script** ([ethers](https://docs.ethers.org/) v6):

```ts
import { Contract, JsonRpcProvider, Wallet } from "ethers";

const provider = new JsonRpcProvider(process.env.BASE_RPC_URL);
const admin = new Wallet(process.env.DAO_ADMIN_KEY!, provider);
const claim = new Contract(
  process.env.DAO_CLAIM_ADDRESS!,
  ["function adminMint(address to)"],
  admin,
);

export async function addMember(to: string) {
  const tx = await claim.adminMint(to);
  return (await tx.wait())?.hash;
}
```

Shipped as a small npm package (name to be decided) with a CLI (`mint <address>`) and a balance check that warns when the key runs low on gas.

**Who generates the key:** the service, locally. It passes only `adminAddress` to `POST /daos`, so the relayer never sees the private key. The UI's create page can generate one in the browser and offer it as a download.

**Limits and why they matter:** this path reintroduces part of the risk Gov's README warns about. A leaked or misused admin key could mint members to swing a vote: anyone minted before a proposal's snapshot can vote on it. The safeguards:

- **Rate cap.** At most `adminCap` mints per `adminPeriod`, so a leaked key can add only a bounded number of votes before the DAO reacts.
- **Expiry.** The admin path stops at `adminExpiry` unless the DAO renews it.
- **Revocation by vote.** The DAO can call `setAdmin(address(0))`, tighten `setAdminLimits`, or remove the whole operator with `nft.setOperator(address(0))`.
- **Visibility.** Every admin mint emits `AdminMinted(to, tokenId)`; the DAO page shows recent admin mints next to open proposals so members can see a sudden wave.
- **Undo.** Members minted improperly can be removed with `govBurn` by vote, as long as honest members still hold a majority.

The cap should stay small relative to the membership: a cap that lets the admin outnumber active voters within one voting delay defeats the point. The UI should warn when `adminCap` exceeds a few percent of current supply.

### Gov extension: proposeBySig

OpenZeppelin's Governor already accepts relayed votes through `castVoteBySig` and `castVoteWithReasonAndParamsBySig`. It has no signature-based `propose`: `propose` checks the proposal threshold against `msg.sender`. Without a change, a member with no ETH could vote but not propose.

Add a `GovProposeBySig` extension to Gov:

```solidity
function proposeBySig(
    address[] memory targets,
    uint256[] memory values,
    bytes[] memory calldatas,
    string memory description,
    address proposer,
    bytes memory signature
) public returns (uint256);
```

It verifies an EIP-712 signature by `proposer` over the proposal's contents and a nonce (reusing `Governor`'s `Nonces`), checks the threshold against `proposer`'s votes, and calls the internal `_propose(..., proposer)`.

`GovProposalTracking` records proposal IDs in its public `propose` override, so `proposeBySig` would bypass it as written. Move the tracking into an override of `_propose` so both paths record the ID.

The alternative, setting the proposal threshold to 0 and letting the relayer call `propose`, works without contract changes but records the relayer as every proposal's author and lets anyone spam proposals. The extension is the recommended route.

### StateRegistry

The manifesto field covers one document. For other data the DAO decides on, a separate contract owned by the Governor keeps Gov minimal:

```solidity
mapping(bytes32 key => bytes32 value) public state;
event StateUpdated(bytes32 indexed key, bytes32 oldValue, bytes32 newValue, uint256 proposalId);
function setState(bytes32 key, bytes32 value) external onlyOwner;
```

Keys are `keccak256` of a human-readable name (`"website.build"`, `"terms.v2"`). The service, or any third party, can read `state(key)` to check that what it serves matches what the community approved. Proposals reach it like any other call, since Gov executes arbitrary calldata.

## Relayer API

The relayer is Wulong extended with dao.new endpoints. Wulong's [SIWE](https://login.xyz/) authentication identifies the service creating a DAO; members authenticate only through their own signatures, which the contracts verify.

| Method | Path | Caller | Does |
| --- | --- | --- | --- |
| `POST` | `/daos` | Service (SIWE) | Generates codes, publishes the tree, calls `createDao`, returns the DAO and codes |
| `GET` | `/daos/:gov` | Anyone | DAO addresses, parameters, claimed count, treasury balance |
| `POST` | `/claims` | Member | Submits a signed claim |
| `POST` | `/proposals` | Member | Submits a signed `proposeBySig` |
| `POST` | `/votes` | Member | Submits a signed `castVoteBySig` |
| `POST` | `/executions` | Anyone | Executes a passed proposal |
| `POST` | `/admin-mints` | Service | Submits a signed `adminMintBySig`, for services that keep their admin key unfunded |

`POST /daos` request:

```json
{
  "name": "Example Community",
  "symbol": "EXC",
  "members": ["alice@example.com", "bob@example.com"],
  "manifestoCid": "bafy...",
  "votingDelay": 86400,
  "votingPeriod": 604800,
  "quorum": 20,
  "claimDays": 30,
  "admin": {
    "address": "0x...",
    "cap": 10,
    "periodDays": 7,
    "expiryDays": 365
  }
}
```

`members` is either an array of emails or an integer. `admin` is optional; without it, new members join only by vote. Response:

```json
{
  "gov": "0x...",
  "nft": "0x...",
  "claim": "0x...",
  "registry": "0x...",
  "treeCid": "bafy...",
  "codes": ["7Hq2...", "..."]
}
```

When emails are given, `codes` is omitted: the relayer emails each link and forgets both lists. Codes are returned once and never stored.

### Relayer constraints to plan for

- **Rate limiting.** Wulong limits each route to 10 requests per minute per client IP. With TLS terminating in the enclave, every client shares the gateway's IP, so a DAO voting at once would hit one shared limit. Relayed member routes need either a higher, separate limit or per-DAO queuing.
- **Gas budget.** Wulong caps its relayer wallet's balance (0.01 ETH by default). Deploys and sponsored transactions need a funding policy: per-service prepaid credit, a per-DAO allowance, or a fee at deploy.
- **Abuse.** Free deploys and free proposals invite spam. Limit deploys per SIWE address and sponsored actions per member per day.
- **Email.** Sending mail needs an outside provider, so addresses leave the enclave at that one step. Encrypt the list to the enclave on upload, keep it only in memory, and drop it after sending.

## UI (Genji)

| Page | Purpose |
| --- | --- |
| `/` | Create a DAO: name, members (emails or number), parameters with sensible defaults |
| `/created/:gov` | Shows addresses and, for the count option, the codes once, with CSV download and a warning that they won't be shown again |
| `/claim/:gov#code` | Creates or opens a passkey wallet, signs the claim, submits it |
| `/join/:gov` | Creates a passkey wallet for a user who joined later and hands its address back to the service for an admin mint |
| `/dao/:gov` | Treasury, members, manifesto, registry state, proposal list |
| `/dao/:gov/propose` | Proposal builder with templates: send funds, set a state hash, update the manifesto, add a member |
| `/dao/:gov/proposal/:id` | Proposal detail, vote, execute |

Proposal templates matter more than a generic calldata editor: most members will only ever send funds or approve a hash.

## Treasury

The Governor holds whatever it receives: ETH or any [ERC-20](https://eips.ethereum.org/EIPS/eip-20) token. For a service that budgets in fiat, a stablecoin such as [EURC](https://www.circle.com/eurc) or [USDC](https://www.circle.com/usdc) keeps the amount understandable to its accounting.

**Fund after the claim window, or after a claim threshold.** Quorum is a fraction of members at each proposal's snapshot. If the treasury is funded while only a few codes are claimed, those few members could pass a transfer before the rest arrive. The UI should show the claimed count and recommend funding once claims are well along.

## Trust model

| Actor | Can | Cannot |
| --- | --- | --- |
| Service | Choose who receives codes; fund or not fund | Vote without a code; take funds back once sent |
| Relayer | Delay or refuse to submit signatures | Forge claims, votes or proposals; keep codes (in the TEE design) |
| Operator (`ClaimOperator`) | Mint for committed codes until the claim deadline; relay admin mints within the cap | Mint for anything else, or past its deadlines and cap |
| Service admin key | Add up to `adminCap` members per period until expiry | Exceed the cap; survive a DAO vote revoking it; remove members |
| Members | Propose, vote, execute; add or remove members by vote | Transfer or sell their vote |

The service picks the first electorate. That is acceptable for this product, and the UI should say it plainly. After the deadline, membership changes only by vote.

A member who loses a passkey can be re-added by a governance vote: burn the old token with `govBurn` and mint to a new address with `safeMint`.

## Default parameters

Suggested defaults, all changeable at deploy and later by vote:

| Parameter | Default | Reason |
| --- | --- | --- |
| Voting delay | 1 day | Time to notice a proposal |
| Voting period | 7 days | Fits non-crypto communities' pace |
| Proposal threshold | 1 vote | Any member can propose |
| Quorum | 20% | Reachable with partial participation |
| Claim window | 30 days | Long enough for email-based onboarding |
| Admin cap | 10 mints per 7 days | Steady growth; bounded damage if the key leaks |
| Admin expiry | 365 days | Forces the DAO to renew the service's power once a year |

## Open questions

- Should a DAO be able to reopen a claim window by vote (new root, new deadline), or should later members always come through `safeMint`?
- Should the relayer sponsor gas indefinitely, or only during an initial period after which the DAO pays for its own relaying from the treasury?
- Should the admin cap be a fixed number or a percentage of current supply, so it scales with the community?
- Is per-member metadata (`tokenURI`) worth exposing, or should every membership share one URI?
- Does the service need an off-chain integration (webhook or SDK) that reads `StateRegistry` and refuses to deploy a non-approved build?

## Milestones

1. **Contracts:** `GovProposeBySig` with tracking moved to `_propose`, `ClaimOperator` (claims and admin path), `StateRegistry`, `DaoFactory`, with [Foundry](https://getfoundry.sh/) tests including front-running, replay and admin-cap cases.
2. **Admin helper:** the npm package and CLI, plus the `/join` handoff.
3. **Relayer:** code generation, tree publishing, `POST /daos` and the sponsored routes in Wulong, with rate limits and gas policy.
4. **UI:** create, claim and proposal pages in Genji.
5. **Pilot:** one real service on Base Sepolia, then Base.

## Further reading

- [OpenZeppelin Governor guide](https://docs.openzeppelin.com/contracts/5.x/governance)
- [EIP-712: typed structured data signing](https://eips.ethereum.org/EIPS/eip-712)
- [ERC-721: non-fungible tokens](https://eips.ethereum.org/EIPS/eip-721)
- [Sybil attack](https://en.wikipedia.org/wiki/Sybil_attack), the risk the service's code distribution must guard against
- [dstack](https://github.com/Dstack-TEE/dstack), the TEE platform Wulong runs on
