DAO API
Manage on-chain governance.
Get DAO Information
GET /dao/:realmId
Retrieve DAO details, treasury, and proposals.
Response
{
"realmId": "REALM123...",
"name": "MyToken DAO",
"symbol": "MTK",
"type": "community",
"governance": "GOV456...",
"treasury": {
"address": "TRES789...",
"balances": {
"MTK": 50000000,
"SOL": 10.5
}
},
"config": {
"quorum": 10,
"passThreshold": 60,
"votingPeriodHours": 72,
"minTokensToCreate": 1000000
},
"proposals": {
"total": 5,
"active": 1,
"passed": 3,
"failed": 1
},
"voters": 142
}List Proposals
GET /dao/:realmId/proposals
List all proposals with optional filters.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
status | string | active, passed, failed, cancelled |
limit | number | Max results (default: 20) |
offset | number | Pagination offset |
Response
{
"proposals": [
{
"proposalId": "PROP123...",
"title": "Increase Marketing Budget",
"description": "...",
"status": "active",
"createdAt": "2026-02-15T12:00:00Z",
"votingEndsAt": "2026-02-18T12:00:00Z",
"creator": "8vP2...",
"voteResults": {
"yes": 500000000,
"no": 100000000,
"total": 600000000,
"quorumMet": true
}
}
],
"pagination": {
"total": 5,
"limit": 20,
"offset": 0
}
}Get Proposal Details
GET /dao/:mint/proposals/:proposal
Detailed view of a proposal including votes.
{
"name": "Release tranche 1",
"description": "Episode 1 shipped",
"state": "voting",
"yesVotes": "1200000000000",
"noVotes": "0",
"votingStartedAt": "2026-09-06T10:00:00.000Z",
"votingEndsAt": null,
"executedAt": null,
"transfer": { "to": "8vP2...", "amount": 1500 },
"milestoneId": "ms-1"
}| Field | Description |
|---|---|
transfer | The treasury transfer stored as the proposal’s transaction #0 — to is the recipient wallet, amount in whole tokens — or null for a signalling proposal |
milestoneId | Studio milestone id parsed from the [milestone:<id>] prefix that createProposal writes into the description (null when absent). The prefix is stripped from description |
Vote weight base and launch-DAO safeguards
Launch DAOs created from this release on:
- Max vote weight =
Absolute(totalSupply − treasury allocation). Everything that can be bought from the pool or claimed from vesting counts; only the treasury (owned by the governance, it can never vote) is excluded.passThreshold% of that is the pass bar — spl-governance v3 has no separate quorum, sodao.quorumis accepted but ignored. - No early tipping. Every proposal stays in voting for the full
votingPeriodHours. - Hold-up: a passed proposal’s transactions become executable after
DAO_HOLD_UP_SECONDS(default 24 h). - Proposing needs 1 % of total supply deposited (the
/txbuilder deposits the shortfall). - Creator veto. The realm has a council mint with a supply of exactly 1 token, held by the launch beneficiary (mint authority revoked). The council can veto any community proposal while it is being voted on (
action: "veto"), but cannot pass or create proposals. - Realm authority is handed from Etch’s wallet to the governance PDA, so realm changes need a (vetoable) proposal.
Realms created before this (circulating-supply SupplyFraction, no council) keep working for every read and user-signed action except veto.
Proposal detail also returns holdUpSeconds, councilVeto (boolean), vetoVotes (raw), and canTipEarly (false on launch DAOs).
Create Proposal
Removed. The custodial
POST /api/dao/:mint/deposit,POST /api/dao/:mint/proposalsandPOST /api/dao/:mint/proposals/:proposalroutes signed with Etch’s own wallet (so the launch owner could vote with Etch-held tokens). They now answer410 { "error": "custodial_route_removed", "details": { "replacement": "/api/dao/:mint/tx" } }. Use the user-signed builder. The GET endpoints are unchanged.
POST /dao/:realmId/proposal
Submit a new governance proposal.
Request Body
{
"title": "Proposal Title (max 80 chars)",
"description": "Detailed description...",
"type": "treasury_transfer",
"actions": [
{
"type": "transfer",
"amount": 100000000000,
"recipient": "WALLET_ADDRESS...",
"mint": "TOKEN_MINT..."
}
],
"options": {
"votingPeriodHours": 48
}
}Proposal Types
| Type | Actions | Description |
|---|---|---|
treasury_transfer | transfer | Send tokens from treasury |
config_change | set_config | Modify governance params |
general | none | Community signaling |
Actions
Transfer:
{
"type": "transfer",
"amount": 100000000000,
"recipient": "8vP2...",
"mint": "TOKEN_MINT..."
}Set Config:
{
"type": "set_config",
"config": {
"quorum": 15,
"passThreshold": 65
}
}Response
{
"proposalId": "PROP789...",
"status": "active",
"transaction": "5abc...",
"votingEndsAt": "2026-02-18T12:00:00Z"
}Cast Vote
POST /dao/:realmId/vote
Vote on an active proposal.
Request Body
{
"proposal": "PROP789...",
"vote": "yes",
"weight": 1000000000 // Optional: specific token amount
}Vote Options
| Value | Description |
|---|---|
yes | Approve the proposal |
no | Reject the proposal |
abstain | Count toward quorum, neutral outcome |
veto | Council-only rejection |
Response
{
"signature": "5ghi...",
"vote": "yes",
"weight": 1000000000,
"proposal": "PROP789...",
"votingPower": 1000000000
}Cancel Proposal
POST /dao/:realmId/proposals/:proposalId/cancel
Cancel a proposal (only by creator before voting starts).
Response
{
"signature": "5jkl...",
"status": "cancelled"
}Execute Proposal
POST /dao/:realmId/proposals/:proposalId/execute
Execute a passed proposal (anyone can trigger after pass).
Response
{
"signature": "5mno...",
"status": "executed",
"results": {
"transfers": [
{ "amount": 100000000000, "recipient": "8vP2..." }
]
}
}Withdraw Governance Tokens
POST /dao/:realmId/withdraw
Withdraw tokens deposited for voting.
Request Body
{
"amount": 1000000000
}Get Voting Power
GET /dao/:realmId/voting-power/:wallet
Get current voting power for a wallet.
Response
{
"wallet": "8vP2...",
"tokenBalance": 1000000000,
"votingPower": 1000000000,
"isDeposited": true,
"canCreateProposal": true
}Build a Governance Transaction (non-custodial)
POST /dao/:mint/tx
The only way to act in a DAO (the custodial endpoints above are gone). Etch builds an unsigned transaction for the authenticated wallet; the wallet signs it and the client sends it. See the Transactions API for the response object and the client-side signing flow.
There is no ownership check on this endpoint: any token holder may deposit,
vote, propose, finalize, execute or cancel — exactly as on-chain governance
allows. (Ownership was only enforced on the custodial routes because Etch
signed there.) :mint is the token mint; the realm and governance are resolved
from the launch.
Request Body
Discriminated on action:
{ "action": "deposit", "amount": 123.45 }
{ "action": "withdraw" }
{ "action": "createProposal", "name": "…", "description": "…", "options": ["Yes", "No"],
"transfer": { "to": "<pubkey>", "amount": 1500 }, "milestoneId": "ms-1" }
{ "action": "vote", "proposal": "<pubkey>", "approve": true }
{ "action": "veto", "proposal": "<pubkey>" }
{ "action": "finalize", "proposal": "<pubkey>" }
{ "action": "execute", "proposal": "<pubkey>", "transactionIndex": 0 }
{ "action": "cancel", "proposal": "<pubkey>" }| Action | Notes |
|---|---|
deposit | amount in whole tokens (9 decimals). Tokens are pulled from the wallet’s own associated token account. On the wallet’s first deposit the token-owner record is created in the same transaction. |
withdraw | Withdraws the wallet’s entire governance deposit back to its ATA. |
createProposal | options defaults to ["Approve", "Deny"]; description may be empty. The proposal is signed off in the same transaction so it is immediately votable. Response adds "proposal": "<pubkey>". transfer (optional, { to, amount } — recipient wallet + whole tokens) inserts an SPL transfer of amount from the DAO’s native treasury ATA to to’s ATA as proposal transaction #0, so execute with transactionIndex: 0 pays it out once the vote passes; the recipient ATA is created idempotently in the create transaction (paid by the proposer). Response then also carries "transactionIndex": 0. Requires the DAO to have a native treasury (404 dao_not_found otherwise). milestoneId (optional, 1-64 chars of [A-Za-z0-9_-], also accepted inside transfer) is stored as a [milestone:<id>] prefix on the description and echoed in the response and in proposal detail. |
vote | approve: true votes for option 0; approve: false is a deny vote. (veto: true is treated as action: "veto".) Response adds "voteRecord". |
veto | Council veto — only the wallet holding the DAO’s council token (the creator) can sign it. Deposits the council token into the realm first when it is not deposited yet, then casts a Veto vote; one veto ends the proposal as vetoed. Only while the proposal is being voted on. Response adds "voteRecord" and "councilMint". 400 no_council on DAOs created before the council; 403 not_council_member for any other wallet. |
finalize | After the voting period ends. The proposer’s token-owner record is resolved from the proposal account. |
execute | Executes the proposal’s transaction at transactionIndex (default 0). The inner instructions are read from the on-chain proposal-transaction account; 404 proposal_transaction_not_found when the proposal has no instruction at that index. |
cancel | Only the proposer’s wallet can sign a valid cancel. |
Response
{
"transaction": "<base64 legacy Transaction, unsigned>",
"version": "legacy",
"feePayer": "<wallet>",
"blockhash": "…",
"lastValidBlockHeight": 123,
"signers": ["<wallet>"],
"summary": "Deposit 123.45 TKN into DAO"
}Errors
| Status | error | Meaning |
|---|---|---|
| 401 | unauthorized | Missing / invalid JWT |
| 400 | invalid_request | Unknown action or invalid payload (details has the issues) |
| 404 | dao_not_found | No launch for this mint, or it has no DAO |
| 404 | proposal_not_found | vote / veto / finalize: the proposal account could not be read |
| 400 | no_council | veto on a DAO created without a council |
| 403 | not_council_member | veto from a wallet that holds no council token |
| 500 | build_failed | RPC / SDK error while building (message) |
Webhook Events
| Event | Description |
|---|---|
dao.proposal_created | New proposal submitted |
dao.vote_cast | Vote recorded |
dao.proposal_passed | Proposal passed quorum |
dao.proposal_failed | Proposal failed/rejected |
dao.proposal_executed | Proposal executed |
dao.proposal_cancelled | Proposal cancelled |
Examples
Create and Vote on Proposal
async function createAndVote(
authToken: string,
realmId: string,
wallet: string
) {
// Create proposal
const proposal = await fetch(
`https://etch.film.fun/api/dao/${realmId}/proposal`,
{
method: "POST",
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
title: "Community Grant #1",
description: "Send 100K tokens to marketing team",
type: "treasury_transfer",
actions: [{
type: "transfer",
amount: 100000000000,
recipient: "MARKETING_WALLET...",
mint: "YOUR_TOKEN_MINT..."
}]
})
}
).then(r => r.json());
console.log(`Created: ${proposal.proposalId}`);
// Vote yes
const vote = await fetch(
`https://etch.film.fun/api/dao/${realmId}/vote`,
{
method: "POST",
headers: {
Authorization: `Bearer ${authToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
proposal: proposal.proposalId,
vote: "yes"
})
}
).then(r => r.json());
console.log(`Voted: ${vote.signature}`);
}Error Codes
| Code | Description |
|---|---|
proposal_not_found | Proposal doesn’t exist |
already_voted | Wallet already voted |
voting_ended | Proposal period closed |
insufficient_tokens | Not enough tokens to create proposal |
not_authorized | Not proposal creator |
not_executable | Proposal conditions not met |