Transactions API
Etch builds, the user signs, Signal sends. For DAO governance and vesting claims Etch never signs on the user’s behalf. Instead it returns an unsigned transaction whose fee payer and every authority is the authenticated wallet. The client signs it with the user’s wallet, sends it, and (optionally) asks Etch to confirm it.
Signal ── POST /api/dao/:mint/tx ──────▶ Etch builds tx (user = fee payer)
◀── { transaction, blockhash, … } ─┘
Signal: deserialize → wallet.signTransaction → sendRawTransaction
Signal ── POST /api/tx/confirm ────────▶ Etch polls → { confirmed, slot }The custodial routes (POST /dao/:mint/deposit, /proposals,
/proposals/:proposal, POST /vesting/:contract) remain available as an
owner-gated fallback and are only used when a /tx endpoint returns 501.
Built transaction response
Every /tx endpoint returns the same object:
{
"transaction": "<base64 legacy Transaction, unsigned by the user>",
"version": "legacy",
"feePayer": "<owner pubkey>",
"blockhash": "…",
"lastValidBlockHeight": 123,
"signers": ["<owner pubkey>"],
"summary": "Deposit 123.45 TKN into DAO"
}| Field | Description |
|---|---|
transaction | Base64-encoded legacy Transaction, serialised with requireAllSignatures: false. Compute-budget instructions (CU limit + priority fee) are prepended. |
version | Always "legacy" today; a 0 would indicate a VersionedTransaction. Deserialize by this field. |
feePayer | The authenticated wallet. It is also every owner / authority in the instructions. |
blockhash, lastValidBlockHeight | Pass both to confirmTransaction (or to POST /api/tx/confirm). |
signers | Public keys that must sign client-side. Only the user — any ephemeral keypair an instruction needs is already partially signed by Etch. |
summary | Human-readable description for the wallet prompt. |
Some actions add extra fields (proposal on createProposal, voteRecord on
vote); they never remove any of the above.
Signing on the client
import { Transaction, VersionedTransaction } from "@solana/web3.js";
const built = await etch.post(`/api/dao/${mint}/tx`, { action: "deposit", amount: 123.45 });
const tx = built.version === "legacy"
? Transaction.from(Buffer.from(built.transaction, "base64"))
: VersionedTransaction.deserialize(Buffer.from(built.transaction, "base64"));
const signed = await wallet.signTransaction(tx);
const signature = await connection.sendRawTransaction(signed.serialize());
await connection.confirmTransaction({
signature,
blockhash: built.blockhash,
lastValidBlockHeight: built.lastValidBlockHeight,
});Confirm a transaction
POST /api/tx/confirm
Optional helper: polls a signature the client already sent to confirmed
commitment using the same polling loop Etch uses for its own transactions.
Clients may confirm locally instead.
Request Body
{
"signature": "5abc…",
"blockhash": "…",
"lastValidBlockHeight": 123
}Response
{ "confirmed": true, "slot": 284219311, "signature": "5abc…" }Errors
| Status | error | Meaning |
|---|---|---|
| 401 | unauthorized | Missing / invalid JWT |
| 400 | invalid_request | Body failed validation |
| 400 | transaction_failed | The cluster processed the transaction and it errored (message has the error) |
| 408 | expired | Block height passed lastValidBlockHeight before confirmation — rebuild and resend |
| 504 | confirm_timeout | Polling gave up (~30s). Safe to retry. |
Endpoints that return built transactions
| Endpoint | Doc |
|---|---|
POST /api/dao/:mint/tx | DAO API → Build a governance transaction |
POST /api/vesting/:contract/tx | Vesting API → Build a claim transaction |