SettleMint
Governance

Governance proposals and polls

Create proposals, run record-date votes, cast ballots, execute approved actions through the timelock, and run off-chain polls with DALP APIs, SDKs, and CLI commands.

Governance proposals and polls

Token governance lets holders vote on decisions about a token. Use this flow to create a proposal or poll, run a record-date vote, collect ballots, execute approved actions through the timelock, and export the result for an audit trail.

A bank or regulated issuer needs the vote, the weights, and the outcome to be reconstructible after the fact. Every on-chain vote you run here is weighted by each holder's voting power at a fixed record date: a snapshot timestamp that freezes how much weight each holder carries, so later trades cannot change a settled vote. Every proposal and poll exposes a CSV export of its ballots and final tally.

DALP exposes governance through the Platform API, the SDK, and the CLI. The API and SDK share the same governance model. Proposals run on-chain through a per-token governor and timelock; polls are off-chain signalling votes whose ballots are wallet-signed messages rather than transactions.

A proposal moves through a fixed lifecycle: run a preflight, create the proposal, let holders cast votes during the voting period, then for an executable proposal queue it on the timelock and execute it after the delay. The sections below follow that order.

For the operator's view of running a vote from the Console, see the Governance votes runbook. For the voting-power capability that supplies vote weights, see the Voting Power feature reference.

When to use this flow

Use this flow when a token carries the voting-power capability and your system has the governance add-on factory available.

The first proposal on a token deploys a dedicated governor and timelock pair for that token. The governor runs the vote; the timelock holds the authority to carry out approved executable actions after a delay. This pair is created inside the create-proposal flow, so a separate deployment step is not required.

Choose the surface that matches the decision:

DecisionUse
A binding For/Against/Abstain decision on-chainBinary proposal
A recorded choice among several options on-chainMulti-option proposal
An on-chain decision that runs token operationsExecutable proposal
A non-binding signal with no gas costPoll

Do not use this page as a substitute for the API Reference. Use the API Reference for exact request and response fields, and the CLI Command Reference for command syntax.

Check prerequisites with a preflight

Before creating a proposal, run a preflight against the token. It confirms three things: whether a governor already exists, whether the token has voting power attached, and, for each executable action you intend to propose, whether the timelock holds the authority to run it.

const preflight = await client.addons.governance.preflight({
  body: {
    tokenAddress: "0xTOKEN",
    actions: [],
  },
});

console.log(preflight.data.governorExists, preflight.data.votingPowerAttached);

The preflight response gives you:

Response fieldHow to use it
governorExistsTells you whether the first proposal will bootstrap the governor and timelock pair.
votingPowerAttachedConfirms the token carries the voting-power capability. Without it, holders have no vote weight.
actionAuthorityReports, per action, whether the timelock has the role and the operation is allowlisted on the governor.
grantRoleCtaCarries the grant-role request payload when the timelock lacks the governance role on the token.

When actionAuthority flags an operation the timelock cannot run, resolve the role grant before you create the proposal. A missing authority is a real create-time block, not a warning.

Create a proposal

Create a proposal with the token address, a voting mode, a title, a description, the record date, the quorum, and the voting period. The create operation runs on-chain and returns the governor address, the timelock address, and the new proposal id.

const created = await client.addons.governance.create({
  body: {
    tokenAddress: "0xTOKEN",
    mode: "binary",
    title: "Adjust the supply cap",
    descriptionRichText: "Raise the supply cap to fund the next issuance round.",
    recordDateAt: new Date(Date.now() + 48 * 60 * 60 * 1000),
    votingPeriodSeconds: 604800,
    quorumBps: 2000,
    approvalThresholdBps: 7500,
    walletVerification,
  },
});

if (!("data" in created)) {
  throw new Error(`Proposal creation is still processing: ${created.statusUrl}`);
}

const { governorAddress, proposalId } = created.data;

The create call accepts these inputs:

InputWhat it sets
modebinary for For/Against/Abstain, or multiOption for a choice among labelled options.
recordDateAtThe voting-power snapshot timestamp. It must be at least 24 hours ahead and at most 90 days ahead.
votingPeriodSecondsThe voting window, from 1 to 30 days. The default is 7 days.
quorumBpsThe share of record-date voting power, in basis points, that must participate for the vote to pass.
approvalThresholdBpsBinary mode. The share of For plus Against, in basis points, that For must reach. 0 keeps a simple majority; 7500 requires a 75% supermajority. Abstain counts toward quorum and never toward this share.
approvalThresholdFloorBpsApplied only when the call also bootstraps the governance pair. It is the minimum approval threshold every later proposal on that governor must meet, so a proposer cannot opt out of a supermajority.
optionsTwo to ten option labels. Required for multiOption, rejected for binary.
actionsUp to ten executable catalog actions. Binary mode only. Omit them for a resolution that records a decision only.
executionDelaySecondsFor executable proposals, the timelock delay before execution, from 1 to 30 days. The default is 2 days.
documentsOptional supporting links or uploaded-file references attached to the proposal.

Multi-option proposals are resolutions only: they capture the winning option and cannot carry executable actions. Executable proposals are always binary.

The record date sets the notice period. Holders who have not yet activated their voting power, by delegating it (usually to themselves), can still do so until the record date. Weight is fixed at the record date: balances acquired afterward do not count, and balances sold afterward still count.

Attach a proposal document

To attach a file, request a presigned upload URL, upload the file to it, then reference the returned object key in the proposal's documents array.

const upload = await client.addons.governance.getUploadUrl({
  body: {
    fileName: "supply-cap-memo.pdf",
    fileSize: 248000,
    mimeType: "application/pdf",
  },
});

// PUT the file bytes to upload.data.uploadUrl with the returned headers,
// then pass upload.data.objectKey in the proposal's documents array.

Uploaded files go to private storage. Retrieve one later with a presigned download URL from getDownloadUrl. Document uploads accept PDF, common image formats, and Office documents up to 25 MB.

Cast a vote

Holders with voting weight greater than zero at the record date can vote once the voting period opens. Use support for a binary proposal and optionIndex for a multi-option proposal. The two are mutually exclusive, and exactly one is required.

await client.addons.governance.castVote({
  body: {
    tokenAddress: "0xTOKEN",
    governorAddress,
    proposalId,
    support: "for",
    reason: "Supports the next issuance round.",
    walletVerification,
  },
});

A holder who delegated their voting power to another wallet has a weight of zero for the vote, even while holding tokens. On-chain votes are final once cast. Quorum counts every cast ballot, including For, Against, Abstain, and every option in a multi-option vote.

Queue and execute an approved proposal

An approved executable proposal does not run on its own. Queue it on the timelock, wait for the configured delay, then execute it. Each step runs on-chain.

await client.addons.governance.queue({
  body: { tokenAddress: "0xTOKEN", governorAddress, proposalId, walletVerification },
});

// After the timelock delay elapses:
await client.addons.governance.execute({
  body: { tokenAddress: "0xTOKEN", governorAddress, proposalId, walletVerification },
});

The timelock holds only the governance role on the token, and the operations it can run are restricted to an allowlisted catalog. A vote can adjust parameters such as caps, fees, yield schedules, and conversion settings. It cannot mint or burn, change roles, rewire compliance and identity, or alter token capabilities. Operations outside the catalog cannot be proposed.

A governance-role holder can cancel a proposal while it is pending or queued. Cancelling a queued proposal also removes the scheduled timelock operation.

await client.addons.governance.cancel({
  body: { tokenAddress: "0xTOKEN", governorAddress, proposalId, walletVerification },
});

Read proposals, ballots, and outcomes

List proposals across the system's governors, filterable by token, status, mode, and proposer. Read a single proposal for its config, tallies, quorum progress, and metadata.

const proposals = await client.addons.governance.list({ query: {} });

const proposal = await client.addons.governance.read({
  params: { governorAddress, proposalId },
});

console.log(proposal.data.status, proposal.data.forVotes, proposal.data.againstVotes, proposal.data.quorumVotes);

A proposal moves through these statuses: pending, active, succeeded, defeated, queued, executed, and canceled. Vote tallies and the quorum denominator are returned as decimal strings to preserve full uint256 precision.

List the individual ballots cast on a proposal, or export the full result as CSV for an audit file.

const ballots = await client.addons.governance.ballots({
  params: { governorAddress, proposalId },
  query: {},
});

const exported = await client.addons.governance.export({
  params: { governorAddress, proposalId },
});

// exported.data.csv holds the ballot and outcome CSV.

The CSV export records each voter, their choice, and their weight at the record date. It includes a disclosure that weights reflect record-date balances in full, including balances that were frozen or paused after the record date.

A proposal created directly on-chain rather than through DALP returns createdExternally as true and carries no platform title or rich description. Such a proposal stays fully operable through this API: holders can cast, and operators can queue, execute, and cancel it.

Read the governance instance

Read the governance instance deployed for a token to check whether a governor and timelock pair exists, and to see the token's governance configuration.

const instance = await client.addons.governance.instanceRead({
  params: { tokenAddress: "0xTOKEN" },
});

console.log(instance.data.governorAddress, instance.data.timelockAddress);

The instance read returns the governor address, timelock address, and the token's governance parameters. If no governor has been deployed for the token yet, the response carries a null governor address.

Run an off-chain poll

A poll records a non-binding signal. Ballots are wallet-signed messages weighted by the holder's voting power at the poll's record date, so they cost no gas. Create a poll with the token address, a title, the options, and the record, open, and close timestamps.

const poll = await client.addons.governance.polls.create({
  body: {
    tokenAddress: "0xTOKEN",
    title: "Preferred coupon frequency",
    options: ["Quarterly", "Semi-annual", "Annual"],
    recordDateAt: new Date(Date.now() + 48 * 60 * 60 * 1000),
    opensAt: new Date(Date.now() + 72 * 60 * 60 * 1000),
    closesAt: new Date(Date.now() + 168 * 60 * 60 * 1000),
  },
});

const pollId = poll.data.id;

The create response returns the poll's immutable definition hash. Holders cast a ballot by signing the poll's typed data and submitting the signature, or by asking DALP to sign with their managed wallet key in one step.

// Custodial: DALP signs with the caller's managed key after wallet verification.
await client.addons.governance.polls.signBallot({
  params: { pollId },
  body: { optionIndex: 0, walletVerification },
});

// Self-signed: submit a ballot signed in your own wallet.
await client.addons.governance.polls.castBallot({
  params: { pollId },
  body: {
    voter: "0xVOTER",
    optionIndex: 0,
    definitionHash: poll.data.definitionHash,
    signature: "0xSIGNATURE",
  },
});

A holder can replace their poll ballot any time before the poll closes; the latest ballot per voter counts. When the poll closes, DALP re-verifies every ballot weight against the indexed snapshot and freezes the tally.

Handle the ballot rate limit

Both ballot submission paths, castBallot and signBallot, carry a per-organization, per-poll, per-participant throttle. A single participant can submit at most 10 ballots to the same poll within any 60-second window. The throttle guards against ballot-stuffing bursts, scoped to one organization, one poll, and one participant, so it never blocks other holders, other polls, or other organizations.

A submission that exceeds the limit returns HTTP 429 with the code GOVERNANCE_BALLOT_RATE_LIMITED (DALP identifier DALP-9164). The response includes a Retry-After header with the wait time in seconds, and the same value appears under data.retryAfterSeconds:

{
  "code": "GOVERNANCE_BALLOT_RATE_LIMITED",
  "status": 429,
  "message": "Poll ballot rate limit exceeded",
  "data": { "retryAfterSeconds": 12 }
}

Read data.retryAfterSeconds (or the Retry-After response header), wait that many seconds, then resubmit. Because a holder can replace a ballot before the poll closes, a client that retries after the wait still records the holder's intended choice. Normal voting, where each holder casts or revises one ballot, never approaches the limit. The throttle trips only under rapid repeated submissions from one participant.

import { DalpSdkError } from "@settlemint/dalp-sdk";

try {
  await client.addons.governance.polls.signBallot({
    params: { pollId },
    body: { optionIndex: 0, walletVerification },
  });
} catch (error) {
  if (
    error instanceof DalpSdkError &&
    error.code === "GOVERNANCE_BALLOT_RATE_LIMITED" &&
    error.retryAfterSeconds !== undefined
  ) {
    await new Promise((resolve) => setTimeout(resolve, error.retryAfterSeconds! * 1000));
    // Resubmit the same ballot.
  }
}

For the full error envelope and retry rules, see Error handling.

const closed = await client.addons.governance.polls.close({
  params: { pollId },
});

console.log(closed.data.status, closed.data.totalBallots);

Read a poll for its current or frozen tally, list polls filterable by token, status, and schedule, or export the poll's ballots and weights as CSV.

const current = await client.addons.governance.polls.read({ params: { pollId } });

const pollExport = await client.addons.governance.polls.export({ params: { pollId } });

Monitor governance state

Governance integrations reconcile state through the list and read endpoints. Treat the indexed payload as your checkpoint: a transaction can be final on-chain before the latest indexed state appears in a list or read response. Poll the list or read endpoint until the expected status, tally, or ballot row appears.

Use the proposal status field to drive the lifecycle: act on succeeded to queue, on queued plus the elapsed delay to execute, and stop on executed, defeated, or canceled.

const page = await client.addons.governance.list({ query: {} });

for (const item of page.data) {
  if (item.status === "succeeded") {
    // Queue the approved executable proposal.
  }
}

Poll error reference

A bank or regulated issuer running a poll needs every rejected ballot to carry a stable, explainable reason for the audit file. Each poll operation returns a documented error with a fixed HTTP status and a stable DALP identifier (the id field on the error envelope and on DalpSdkError), so an integration branches on a fixed value rather than parsing a message. Each identifier maps to a symbolic code in the DALP error index, and its full envelope, retryability, and remediation appear on the Platform API error reference.

Create a poll

ErrorStatusWhat DALP observedState change and caller response
DALP-9148422The recordDateAt, opensAt, and closesAt timestamps do not form a valid window.No poll is created. Set recordDateAt before opensAt, and opensAt before closesAt, then retry.
DALP-9147403The caller does not hold the governance role on the polled token.No poll is created. Ask a token admin to grant the governance role to one of your wallets.
DALP-9162422The token does not carry the voting-power feature that supplies ballot weights.No poll is created. Attach the voting-power feature to the token, then retry.

Cast or sign a ballot

ErrorStatusWhat DALP observedState change and caller response
DALP-0080422The submitted optionIndex is outside the poll's list of options.No ballot is recorded. Submit an optionIndex within the poll's option range, then retry.
DALP-9140404The poll identifier does not resolve in the authenticated tenant scope.No ballot is recorded. Verify the poll identifier and tenant context, then retry.
DALP-9143409The signed ballot's definition hash does not match the poll's immutable definition.No ballot is recorded. Fetch the poll again and re-sign over its current definitionHash.
DALP-9145403The claimed voter wallet is not in the authenticated caller's wallet set.No ballot is recorded. Submit the ballot from the account that owns the voter wallet.
DALP-9144422The signature does not verify against the claimed voter address.No ballot is recorded. Sign the exact PollBallot typed data with the voter wallet; smart wallets must be deployed on-chain.
DALP-9146422The voter has zero voting power in the indexed snapshot at the poll's record date.No ballot is recorded. Only wallets with delegated voting power at the record date can vote on this poll.
DALP-9141409The ballot arrived while the poll status was not open, or outside the open and close window.No ballot is recorded. A ballot submitted after the close time is rejected even if it was signed earlier.
DALP-9164429More than ten ballots arrived from one participant for one poll within 60 seconds.No ballot is recorded for the throttled submission. Wait for the Retry-After interval, then resubmit. See Handle the ballot rate limit.
DALP-9163503The signer returned a non-hex signature while signing the ballot for signBallot.No ballot is recorded. Retry after a short backoff; if signing keeps failing, escalate with the request id.

Close a poll

ErrorStatusWhat DALP observedState change and caller response
DALP-9140404The poll identifier does not resolve in the authenticated tenant scope.The poll does not close. Verify the poll identifier and tenant context, then retry.
DALP-9147403The caller does not hold the governance role on the polled token.The poll stays open. Ask a token admin to grant the governance role, then retry the close.
DALP-9142409The poll's tally is already frozen, so the poll cannot close again.Nothing changes. Read the poll to inspect the frozen tally or export the ballots.

CLI coverage

The DALP CLI covers the full governance lifecycle under dalp governance, and off-chain polls under dalp governance-polls:

dalp governance list
dalp governance read --governor-address 0xGOVERNOR --proposal-id 123
dalp governance preflight --token-address 0xTOKEN
dalp governance create --token-address 0xTOKEN --mode binary --title "Adjust the supply cap" --description-rich-text "Raise the supply cap." --record-date-at $(date -u -d '+48 hours' +%Y-%m-%dT%H:%M:%SZ) --quorum-bps 2000
dalp governance cast-vote --token-address 0xTOKEN --governor-address 0xGOVERNOR --proposal-id 123 --support for
dalp governance queue --token-address 0xTOKEN --governor-address 0xGOVERNOR --proposal-id 123
dalp governance execute --token-address 0xTOKEN --governor-address 0xGOVERNOR --proposal-id 123
dalp governance cancel --token-address 0xTOKEN --governor-address 0xGOVERNOR --proposal-id 123
dalp governance ballots --governor-address 0xGOVERNOR --proposal-id 123
dalp governance export --governor-address 0xGOVERNOR --proposal-id 123 --output proposal-123.csv

dalp governance-polls create --token-address 0xTOKEN --title "Preferred coupon frequency" --options '["Quarterly","Semi-annual","Annual"]' --record-date-at 2026-07-01T00:00:00Z --opens-at 2026-07-02T00:00:00Z --closes-at 2026-07-09T00:00:00Z
dalp governance-polls list
dalp governance-polls read 00000000-0000-0000-0000-000000000000
dalp governance-polls sign-ballot --poll-id 00000000-0000-0000-0000-000000000000 --option-index 0 --verification-code 123456
dalp governance-polls close 00000000-0000-0000-0000-000000000000
dalp governance-polls export --poll-id 00000000-0000-0000-0000-000000000000 --output poll.csv

On this page