Skip to main content
A SHHH multisig wallet executes a call only after N of its M owners sign it. This guide shows how to drive that flow — propose → approve → execute — from your own application, using two packages:
The multisig endpoints live in your Chipi backend at /v1/treasuries/:treasuryId/.... You drive them with your own API keys — no Chipi dashboard required.

How authorization works

Each proposal carries an OutsideExecution (the Call[] to run) plus, per owner, that owner’s signature over the execution hash. The coordination backend splits auth by step: Because propose/sign need no secret, an owner can sign in the browser and the key never leaves their device. execute/list use your sk_, so they belong on a server (a Next.js route handler, your backend) — never in client code.

1. Define the actions owners can propose

An ActionTemplate turns typed form values into the contract Call[] and a human title. Use the built-in voteTemplate, or define your own with defineActions:
lib/actions.ts
defineActions validates that every key is unique. meta is optional context your approver UI can render (e.g. { poolId }).

2. Implement a signer

A MultisigSigner produces an owner’s inner envelope for a given execution hash — the key never leaves it — and resolves that owner’s index in the on-chain owner set. The sign callback receives { messageHash, ownerIndex } and returns the envelope felts (bigint[]). For a STARK owner (e.g. a programmatic/agent signer), @chipi-stack/backend builds the envelope for you:
lib/signer.ts
For a passkey owner (WEBAUTHN_P256), the device’s Face ID / Touch ID produces the assertion in the browser — the private key never leaves the secure enclave. @chipi-stack/chipi-passkey prompts for the assertion and @chipi-stack/backend packs it into the envelope:
lib/passkey-signer.ts
For a MetaMask owner (EIP191_SECP256K1), the wallet’s personal_sign produces the signature — MetaMask prefixes and keccak-hashes internally, and the on-chain verifier reproduces the same recipe. The wallet must sign the exact 32 big-endian bytes of the execution hash:
lib/metamask-signer.ts
For a Phantom owner (ED25519), the wallet signs the 64-byte hex-ASCII encoding of the hash (ed25519SignedBytes) — that’s what the user sees in the popup, and what the on-chain verifier reconstructs:
lib/phantom-signer.ts
Helper names below are wrong for 14.8. There are no *OwnerPubkeyParam exports — the shipped helpers are webauthnPubkeyFelts / eip191PubkeyFelts / ed25519PubkeyFelts / starkPubkeyFelts (from @chipi-stack/backend), each returning bigint[]. For the transport’s ownerPubkey, STARK is a single 0x-hex felt; coordinate kinds derive from the corresponding *PubkeyFelts. This section needs a smoke-tested rewrite (tracked: docs audit 2026-06-09) — don’t copy the *OwnerPubkeyParam calls verbatim.
All four launch signer kinds — STARK, WEBAUTHN_P256 (passkey), EIP191_SECP256K1 (MetaMask), and ED25519 (Phantom) — are verified live on propose / sign.

3. The transport

sdk.treasury.forTreasury(...) returns a transport bound to one treasury and one owner’s public key. It implements exactly the contract the React hooks expect (MultisigTransport), so you can wire it straight in — or call its methods directly from a server.
lib/chipi.ts

4. Drop-in components

Not shipped in 14.8. <ProposeAction>, <Approvals>, and <ApprovalInbox> are NOT exported by @chipi-stack/chipi-react/multisig today — only the hooks (useProposeAction, useTreasuryProposals) and helpers ship. The component examples below are roadmap; build your UI on the hooks for now. (Tracked: docs audit 2026-06-09.)
If you don’t want to build UI on the hooks, three embeddable components ship in the same multisig subpath — themeable, mobile-first, no CSS imports or Tailwind required (a namespaced stylesheet injects at runtime):
Theme them to match your app with CSS-var tokens (defaults are Chipi’s neobrutalism):
<Approvals> resolves the device’s owner index via your signer’s resolveOwnerIndex; on a device that isn’t an owner it degrades to read-only. Stamp meta in your template (meta: (v) => ({ poolId: v.poolId })) and it round-trips through the backend to every approver’s renderContext.

5. Risk tiers & lifecycle webhooks

Risk tiers (per-action quorum)

Mark a template’s tier once and configure per-tier quorums on the treasury — routine actions (frequent, low-stakes) clear with fewer signatures than sensitive ones:
When the treasury has the matching quorum configured (dashboard → treasury policy, or PATCH /v1/organizations/:id/treasuries/:treasuryId/policy), the backend overrides requiredApprovals at propose — the client’s value is advisory. Honest scope: this is app-level policy for human treasuries. The wallet’s on-chain threshold remains the hard floor at execute, and agent autonomy is never gated on this layer (contract-enforced caps are the agent tier).

Lifecycle webhooks

Register a webhook on the API key your treasury’s wallet was created under (the standard Chipi /webhooks registration) and subscribe to the governance events — or leave the events list empty to receive everything: Payload: { event, data: { treasuryId, proposalId, title, meta, risk, approvals: { have, need }, status, executedTxHash }, ts }meta is your template’s stamped context, so you can route the notification without a read-back. Verify the chipi-signature header the same way as every Chipi webhook (HMAC-SHA256 of the raw body with your whsec-… signing key):
Delivery is at-least-once with a single retry — make your handler idempotent on (event, proposalId).

6. AI agents as constrained proposers

An AI agent is just one more owner of the multisig — a constrained one. Add its STARK key as an owner, register it with a daily budget, and the coordination layer treats that budget as a dynamic approval rule:
  • An in-budget USDC payment the agent proposes auto-approves (quorum 1) → the treasury.proposal.executable webhook fires → your server executes it. The agent acts alone, within its allowance.
  • Over budget, a non-USDC payment, or any contract action escalates to the human threshold and lands in <ApprovalInbox> as “Vela wants: Move $500 to Endur”.
Register the agent (dashboard → treasury → agents, or the API). The default budget is 0 — the agent can’t move a cent until you raise it:
The agent proposes exactly like any owner — a STARK MultisigSigner at its owner index:
agent-loop.ts
In the inbox, mark agent rows so the founder sees whose action it is:
Daily budgets are app-level policy: they make a misbehaving agent loud (escalation), not impossible. A valid agent signature on an in-budget payment executes without a human. For hard, on-chain-enforced spending caps — the real safety boundary for autonomous agents — use session-key policies (coming in a future release). Size the budget to what you’d let the agent move unattended.

Next.js quickstart

The browser can run propose/sign (pk_ + owner signature) directly, but list/execute need sk_, so they go through your route handlers. The signer always runs client-side — the key never reaches your server.

Route handlers (server, sk_)

app/api/treasury/[id]/proposals/route.ts
app/api/treasury/[id]/transactions/[txId]/execute/route.ts

Client component (browser-direct propose/sign, proxied list/execute)

app/treasury/[id]/page.tsx
That’s the whole loop: propose.mutate({ template, values }) builds the calls, signs the OE as the proposer, and submits it; approve.mutate(proposal) adds another owner’s signature; once signatures.length >= requiredApprovals the proposal is APPROVED and execute.mutate(proposal) relays it on-chain.

Server-only (programmatic) flow

For an agent or backend with no UI, skip the hooks and call the pure helpers with the same transport — every owner is a server-held key:

Notes

  • Free executes are metered. sdk.treasury.forTreasury(...).getMultisigAllowance() returns the org’s remaining free executes before you build a proposal.
  • amount for non-payment actions. Templates that aren’t token payments (a vote, a borrow) carry the intent in calls + title; the payment fields are placeholders.
  • The chain is the source of truth. Off-chain signature verification stops a public pk_ from spamming forged proposals, but the on-chain dispatcher is the authoritative gate at execute — a proposal without a valid N-of-M envelope simply reverts.