Depute

Binding Approval

High-stakes intent gate. Displays a signature your backend supplies.

Answers: Binding Approval, Responsibility Diffusion.

Install

npx ax-depute@latest add binding-approval
pnpm dlx ax-depute@latest add binding-approval
yarn dlx ax-depute@latest add binding-approval
bunx ax-depute@latest add binding-approval

Overview

Unlike a standard ApprovalGate used for day-to-day coordination, BindingApproval provides a heavy-duty friction boundary for irreversible or financial actions. It forces explicit acknowledgment of terms and implements a multi-phase isSigning visual state.

BindingApproval does not sign, hash, or talk to a ledger. Pass signature and hash when your backend has already produced them, and pass onVerify if the app should check that proof. The component only displays those values.

Interactive StorybookView all states, toggle props, and test edge cases.

Basic Usage

<BindingApproval
  title="Wire Transfer Authorization"
  description="Execute a $5,000 wire transfer."
  impactStatement="$5,000.00 will be transferred immediately."
  signerIdentity="alice@company.com"
  terms={[
    { id: 't1', text: 'I authorize the transfer of $5,000.00', acknowledged: false },
    { id: 't2', text: 'I understand this action is irreversible', acknowledged: false },
  ]}
  signature={backendSignature}
  hash={backendHash}
  onSign={() => backend.recordIntent()}
  onVerify={(proof) => backend.verify(proof)}
/>

Solution Patterns

This primitive replaces ApprovalGate in high-stakes nodes. The internal state expects a clear distinction between reviewing and signing, where isSigning: true disables interaction and renders a deterministic loading state while your backend records the intent.

Props

PropTypeDefaultDescription
titlestring—Title of the binding action
descriptionstring—Detailed explanation of the action
impactStatementstring—Estimated impact (e.g., "$5,000 will be transferred")
status'reviewing' | 'signing' | 'signed' | 'rejected' | 'expired''reviewing'Current status
termsBindingTerm[]—Terms that must be acknowledged
requireAllTermsbooleantrueRequire all terms acknowledged to sign
isSigningbooleanfalseToggles the active signing visual state
signerIdentitystring—Identity of the approver
timeoutSecondsnumber—Signing window timeout
onSign() => void—Called when the user confirms. Your backend records the intent.
onReject(reason?: string) => void—Called when rejected
signaturestring—Signature string from your backend. Display only.
hashstring—Hash string from your backend. Display only.
onVerify(proof) => void—Your app's check for the displayed signature and hash.

Design rationale

When to use ApprovalGate vs BindingApproval:

ApprovalGateBindingApproval
Use whenThe action is reversible, operational, or low-to-moderate stakesThe action is irreversible or financial, and your backend will record the intent
Friction levelQuick scoped grant — review and approveMandatory term acknowledgment + signing flow
State modelpending → approved / rejectedreviewing → signing → signed / rejected
Examples"Send this email", "Deploy to staging", "Run this query""Wire $50k", "Sign this contract", "Commit to ledger"

Use ApprovalGate by default. Escalate to BindingApproval only when the action is irreversible and your backend must record who confirmed it.

Why isSigning? Normal approvals execute instantly. High-stakes approvals often wait on a backend round trip. isSigning freezes the component so the user cannot submit twice. The component still does not produce a signature.

Why explicit terms? Click-fatigue trains users to blindly click "Approve." Forcing the user to manually acknowledge explicit terms (e.g. "This action cannot be undone") breaks automatic behavior and captures verifiable intent.

On this page