Guilds Module
Location: apps/api/src/modules/guilds/ (treasury: apps/api/src/modules/economy/guild-treasury.service.ts)
Aggregate roots: Guild, GuildMember, GuildApplication, GuildProposal, GuildVote, GuildTreasury, GuildLedgerEntry (append-only)
Merit-gated, self-governing interest-domain communities (ADR-006). Reputation gating goes through the shared AccessMatrixService — never a direct import of the reputation module.
Founding & Membership
- Founding is rep-gated:
AccessMatrixService.canProposeGuildrequires rep ≥ 50 (GUILD_FOUND_THRESHOLD) in the target interest domain. - One active guild per interest — the canonical steward; a second
createfor the same interest returns 409. - Roles:
founder|council|member. Join policy isapply; applying requires rep ≥guild.minReputation(default 20, ceiling 1000) in the guild's interest. The rep gate is re-checked on approval. - The founder cannot leave — archive instead (soft delete). Council can't expel council; nobody expels the founder.
Governance (Inc 2 + 3)
Any member can open a proposal. 6 kinds: admit_member, grant_council, revoke_council, amend_charter, moderation, expel_member (target-kinds require payload.targetId).
| Parameter | Value |
|---|---|
| Vote window | 72 h (closesAt), lazily expired on read/vote |
| Quorum | 50 % of memberCount — by distinct voter count |
| Threshold | 60 % — by rep-weighted for / (for + against) |
| Vote weight | max(1, log₂(rep + 1)) from ReputationSnapshot in the guild's interest |
| Vote values | for | against | abstain (abstain counts for quorum, not threshold) |
Votes are upserted (changeable while open); weight is re-evaluated on every change. Tallies are rebuilt from authoritative DB state inside a transaction. When a proposal passes, its effect executes in the same transaction (membership/role/charter change) and status moves passed → executed. Weight examples: rep 0 → 1.0, rep 10 → ~3.5, rep 100 → ~6.7.
Treasury (Wave 5)
Pooled mana per guild, living in the economy context (same EmojiWallet mana). GuildLedgerEntry is append-only — every movement is a new contribution/disbursement row, never an UPDATE.
contribute— any member, 1–10 000 mana; locks the contributor's wallet row (SELECT ... FOR UPDATE) before the balance check.disburse— founder only, to a member; locks the treasury rowFOR UPDATEso two concurrent disbursements can't both pass the balance check and overdraw.distributeByContribution— founder splitstotalManaacross members weighted by lifetime contribution share (from the ledger), largest-remainder allocation so splits sum exactly; same treasuryFOR UPDATElock. Members who never contributed get nothing.
HTTP Endpoints
All under /api/v1, JwtAuthGuard.
| Method | Path | Description |
|---|---|---|
| POST | /guilds | Found a guild (rep ≥ 50 in domain) |
| GET | /guilds?interestId=&q=&limit= | Discover guilds |
| GET | /guilds/mine | Guilds the viewer belongs to |
| GET | /guilds/:id | Detail + viewer relationship + canApply |
| PATCH | /guilds/:id | Founder/council edits metadata |
| DELETE | /guilds/:id | Founder archives |
| POST | /guilds/:id/apply | Apply to join (rep-gated) |
| DELETE | /guilds/:id/apply | Cancel my pending application |
| POST | /guilds/applications/:applicationId/respond | Approve/reject (manager) |
| GET | /guilds/:id/applications | Pending applications (manager) |
| POST | /guilds/:id/leave | Leave the guild |
| DELETE | /guilds/:id/members/:memberId | Expel (manager) |
| PATCH | /guilds/:id/members/:memberId/role | Promote/demote council ⇄ member (founder) |
| POST | /guilds/:id/proposals | Open a governance proposal (member) |
| GET | /guilds/:id/proposals?status= | List proposals (member) |
| GET | /guilds/proposals/:proposalId | Proposal detail + my vote (member) |
| POST | /guilds/proposals/:proposalId/vote | Cast or change a vote (member) |
| GET | /treasury/:guildId | Treasury balance + last 30 ledger entries |
| POST | /treasury/:guildId/contribute | Member contributes mana |
| POST | /treasury/:guildId/disburse | Founder pays a member |
| POST | /treasury/:guildId/distribute | Contribution-weighted distribution |
Events Emitted
| Event | Payload |
|---|---|
guilds.guild.founded | { guildId, founderId } |
guilds.member.joined | { guildId, memberId } |
guilds.application.submitted | { guildId, userId, applicationId } |
guilds.application.approved | { guildId, userId } |
guilds.proposal.opened | { guildId, proposalId, proposerId } |
guilds.proposal.executed | { proposalId, guildId, kind } |
Admin
/admin/guilds (list, archive/restore), /admin/guilds/:id detail, expel member, resolve applications, force-close proposals — all audit-logged.
See also: /modules/economy (mana/wallets), /modules/reputation, /modules/access.