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 (theCall[] 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
AnActionTemplate 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
AMultisigSigner 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
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
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
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
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
If you don’t want to build UI on the hooks, three embeddable components ship in the samemultisig subpath — themeable, mobile-first, no CSS imports or Tailwind required (a
namespaced stylesheet injects at runtime):
<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: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):
(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.executablewebhook 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”.
0 — the agent can’t move a cent until you raise it:
MultisigSigner at its owner index:
agent-loop.ts
Next.js quickstart
The browser can runpropose/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
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. amountfor non-payment actions. Templates that aren’t token payments (a vote, a borrow) carry the intent incalls+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 atexecute— a proposal without a valid N-of-M envelope simply reverts.
