---
title: "Confirm sensitive actions with a signed approval"
description: "Make an action need a passkey signature over its exact amount and payee, and require a second approver for the most critical ones."
section: "guides"
status: "Planned for 1.2"
spec: "signed-approvals"
canonical: "https://mainsdk.dev/docs/guides/confirm-actions-with-signed-approvals"
updated: "2026-10-28"
---

# Confirm sensitive actions with a signed approval

> Make an action need a passkey signature over its exact amount and payee, and require a second approver for the most critical ones.

Status: Planned for 1.2 · Canonical: https://mainsdk.dev/docs/guides/confirm-actions-with-signed-approvals · Updated: 2026-10-28

You will mark an action so it only runs after a person signs this exact call with a passkey. It takes about 20 minutes.

## You need

- A MainSDK app with passkeys (`auth` with WebAuthn) and an `action` that moves money or changes access.

## 1. Mark the action

```ts title="actions/payouts.ts"
import { action } from "mainsdk";
import { s } from "mainsdk/schema";

export const sendPayout = action({
  name: "payouts.send",
  intent: "Send a payout to a payee",
  input: s.object({ amount: s.number(), payee: s.string() }),
  risk: "destructive",
  confirm: "signed",
  signs: (input) => ({ amount: { value: input.amount, currency: "INR" }, payee: input.payee }),
  run: async ({ input }) => ({ sent: input.payee }),
});
```

An action marked `confirm: "signed"` fails with `MAIN-AUTH-0204` until the call carries an approval that was made for the same action, amount and payee. Admin role changes and Fleet's critical actions always need one.

## 2. Ask the person to sign

`Approvals` from `mainsdk/confirm` has `begin` and `finish`. `begin` returns a WebAuthn challenge that is a hash of the canonical action (name, amount, currency, payee, a nonce and an expiry), together with the sentence to show, such as "Send INR 1,25,000 to acct_7Q…". The person's passkey signs the challenge, and `finish` verifies the assertion and stores an approval that lives five minutes and can be spent once.

## 3. Add a second approver where it matters

`ApprovalQuorum` runs an action only after two different people have approved it. A payment in a market that requires dynamic linking (PSD2, RBI) records the assurance level of the approval in the audit event, which `toOcsfApproval` from `mainsdk/confirm-ocsf` turns into an OCSF record.

## 4. Agents

An agent calling the action over MCP gets an elicitation back instead of a result, naming the action and the amount to show its human. The approval comes back in `_meta.approval` on the retry.

## Check it worked

Call the action without an approval and expect `MAIN-AUTH-0204`. Then approve it with the fake authenticator from `@mainsdk/testing` and expect it to run once, and fail the second time.
