Binding Approval
High-stakes intent gate. Displays a signature your backend supplies.
Answers: Binding Approval, Responsibility Diffusion.
Install
npx ax-depute@latest add binding-approvalpnpm dlx ax-depute@latest add binding-approvalyarn dlx ax-depute@latest add binding-approvalbunx ax-depute@latest add binding-approvalOverview
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.
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
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | — | Title of the binding action |
description | string | — | Detailed explanation of the action |
impactStatement | string | — | Estimated impact (e.g., "$5,000 will be transferred") |
status | 'reviewing' | 'signing' | 'signed' | 'rejected' | 'expired' | 'reviewing' | Current status |
terms | BindingTerm[] | — | Terms that must be acknowledged |
requireAllTerms | boolean | true | Require all terms acknowledged to sign |
isSigning | boolean | false | Toggles the active signing visual state |
signerIdentity | string | — | Identity of the approver |
timeoutSeconds | number | — | Signing window timeout |
onSign | () => void | — | Called when the user confirms. Your backend records the intent. |
onReject | (reason?: string) => void | — | Called when rejected |
signature | string | — | Signature string from your backend. Display only. |
hash | string | — | 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:
ApprovalGate | BindingApproval | |
|---|---|---|
| Use when | The action is reversible, operational, or low-to-moderate stakes | The action is irreversible or financial, and your backend will record the intent |
| Friction level | Quick scoped grant — review and approve | Mandatory term acknowledgment + signing flow |
| State model | pending → approved / rejected | reviewing → 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.
