Business

Confirmation Rules for Agent Actions That Move Money

Antonio Evans9 min read

A split scene: on the left, an AI agent reaches a checkout step and the screen shows a locked payment button with a required checkbox; on the right, a clear yes/no decision tree with two branches.
Key points

Key points

  1. 1The decision rule is reversibility times impact: any action that moves money and cannot be undone in one step needs an explicit yes from a named human before the tool executes.
  2. 2The agent should prepare the payment and the gate should sit in the tool, so a missing, expired or mismatched confirmation token returns an error with no side effect.
  3. 3Confirmation tokens must be bound to the specific amount, payee, currency and purpose, because a generic "approved" flag can be replayed against a different transaction.
  4. 4Audit records and retry handling are part of the gate, not an afterthought: every rejected call should leave a trace, and retries must never convert a "no" into a "yes".

Why the tool call, not the model, is where the gate belongs

Most teams start by writing a careful system prompt: "Never move money without asking the customer first." That is a policy, not a control. Language models can be steered off script by an unusual request, a long context, or a prompt injection hidden in a support ticket, and Bedrock's own engineering write-up notes that agent errors can occur at multiple stages, including wrong tool selection and correct tools called with incorrect parameters (the AWS Machine Learning blog). A prompt cannot enforce anything a model might ignore.

So the reliable boundary is the function that executes. If execute_payment() refuses to run unless it receives a valid confirmation token, the worst a confused or manipulated model can do is generate a rejected call. Agno's documentation frames this as tool-level oversight: a checkpoint right before a specific function runs, where a human can approve, reject or supply additional input before anything happens (the Agno guide). The same pattern exists across frameworks, which is a good sign it is the correct architecture rather than a vendor quirk.

There is also a practical argument. Support and finance teams already think in terms of approval limits and escalation tiers. Dialpad's escalation guidance describes hierarchical escalation as the route for "a refund above the agent's approval limit," and functional escalation as the route to the team with the right skills (the Dialpad escalation guide). A tool gate is simply the machine-readable version of that matrix: the limit lives in code, and crossing it forces a handoff.

The decision rule: reversibility first, impact second

You do not need a long list of action names. You need two questions and a tie-breaker.

Question 1: can this action be undone in a single step, by the same agent, without contacting anyone? A draft, a quote, a saved payment method, an internal note: all reversible. A debit, a wire, a refund posted to a card, a payout to a third party: not reversible. Reversibility is about the outside world, not your database. A refund that takes three to five business days to claw back is not reversible in practice.

Question 2: what is the blast radius if the amount or payee is wrong? Amount matters, recipient matters, and repetition matters. A small refund ten thousand times is a large loss, which is why the rule has to consider velocity as well as size.

Then a tie-breaker: who bears the loss if the action is wrong? If the customer loses money, you need the customer's yes. If your company loses money, you need an internal reviewer. If both, you need both, which is the case for a disputed charge refunded to a customer.

The tiers that follow are deliberate. The status of an action is set by the product team, not left open for a model to pick, and the table below is worth copying into your design doc.

TierCharacteristicWho confirmsExampleGate behavior
0Reversible, no money movedNobodyDraft an invoice, save a payout method, compute a refund quoteExecutes immediately, logged
1Moves money, small, reversible within policy windowNamed customerRefund inside standard policy to the original payment methodToken to the customer, verified against card on file
2Moves money, above the stated limit, or new payeeNamed reviewer in finance or opsRefund above limit, goodwill credit to a different accountToken to a reviewer, requires role check
3Irreversible and externalReviewer plus second approverOutbound wire, payout to a new bank account, refund to a third partyTwo tokens, both bound to the same transaction hash

The important detail is the tier boundary. Escalation matrices in support operations work the same way: knowmax's write-up on escalation matrices describes the core promise as naming the owner of an issue at each level, the time that owner has to act, and the person who gets the case next (the Knowmax escalation matrix guide). Your tier table is that matrix, applied to money.

One caution on thresholds. A limit set in a spreadsheet tends to drift. Review it whenever you change a payee allowlist or a refund policy, and log the review alongside the code change.

What the confirmation token must contain

A boolean confirmed: true is the most common mistake in this area. It is trivially replayed, trivially forged by a model that learned the schema, and it tells you nothing about what was confirmed. The token has to be a signed artifact that describes the transaction it authorizes.

At minimum, bind these fields:

  • A unique token ID and the transaction ID it was issued for.
  • Amount in minor units, currency, and payee identity.
  • The customer or reviewer identity and the role that authorized it.
  • Issued-at and expires-at timestamps, kept short (minutes, not hours).
  • The channel used for confirmation, so you can distinguish a push notification from an in-chat "yes".

The reasoning is the same as any delegated authorization design. Scalekit's discussion of tool calling authentication points out that OAuth was built around human-in-the-loop consent, with browser redirects and scope approval, while agents run in the background and need delegated, short-lived credentials with audit trails (Scalekit on tool calling authentication). A confirmation token is a narrower version of the same idea: delegated permission for one transaction, not for a class of transactions.

For the customer-facing case, the standard protocol is CIBA, client-initiated backchannel authentication, which Auth0 describes as asynchronous authorization where the agent requests approval, the authorization server notifies the user on their device, and the agent proceeds only if authorization is granted (Auth0 on human in the loop for AI agents). That is a much better fit than a chat message, because the approval happens out of band, on a device you trust more than the conversation, and the request can state the exact amount and payee.

Two design rules that save pain later. First, tokens are single-use and consumed inside the same atomic step as the debit. Second, if the amount or payee changes by even one minor unit, the token is void. No "close enough" matching.

Worked example: a payment tool that fails closed

Here is a deliberately small implementation. The point is not the code style; it is that every failure path returns without touching the ledger. A call that arrives without a valid token is rejected, not downgraded, not partially executed, not queued for later.

from dataclasses import dataclass
from decimal import Decimal

@dataclass(frozen=True)
class Confirmation:
    token_id: str
    txn_id: str
    amount_minor: int
    currency: str
    payee_id: str
    subject_id: str      # customer or reviewer identity
    channel: str

class ConfirmationError(Exception):
    """Raised when the gate refuses execution. No ledger writes occur."""

def execute_payment(txn_id: str, payee_id: str, amount_minor: int,
                    currency: str, token: str | None) -> str:
    # 1. Gather the exact facts the token must match.
    facts = look_up_transaction(txn_id)          # server-side, never model-supplied
    if facts is None:
        raise ConfirmationError("unknown_transaction")

    # 2. Missing token: reject before any side effect.
    if not token:
        audit("payment.blocked", reason="missing_token", txn_id=txn_id)
        raise ConfirmationError("confirmation_required")

    conf = verify_signature_and_load(token)      # fails closed on bad signature
    if conf is None:
        audit("payment.blocked", reason="invalid_token", txn_id=txn_id)
        raise ConfirmationError("invalid_confirmation")

    # 3. Bind check: the token must describe this exact transaction.
    if (conf.txn_id != txn_id
            or conf.amount_minor != facts.amount_minor
            or conf.currency != facts.currency
            or conf.payee_id != payee_id
            or amount_minor != facts.amount_minor
            or currency != facts.currency):
        audit("payment.blocked", reason="token_mismatch", txn_id=txn_id)
        raise ConfirmationError("confirmation_mismatch")

    # 4. Freshness and authority.
    if now() > conf.expires_at:
        audit("payment.blocked", reason="expired", txn_id=txn_id)
        raise ConfirmationError("confirmation_expired")
    if not authorize_subject(conf.subject_id, facts.required_role):
        audit("payment.blocked", reason="unauthorized_subject", txn_id=txn_id)
        raise ConfirmationError("unauthorized")

    # 5. Consume token and write ledger in one transaction.
    with ledger.transaction():
        if not consume_token_once(conf.token_id):
            audit("payment.blocked", reason="token_replay", txn_id=txn_id)
            raise ConfirmationError("confirmation_reused")
        ledger.post(txn_id, payee_id, facts.amount_minor, currency)

    audit("payment.executed", txn_id=txn_id, token_id=conf.token_id,
          subject=conf.subject_id, channel=conf.channel)
    return "posting_created"

Three things to notice, because they are the difference between a gate and a decoration.

The facts come from look_up_transaction, not from the model's arguments. The model may propose an amount, but the gate compares the proposal to the server's record and to the token. A model that "rounds" 490.00 to 500.00 gets a mismatch error.

The token is consumed inside the same transaction as the ledger post. If the process dies between consumption and posting, you roll back and the customer can re-confirm. If you consume first and post later, a crash leaves a burned token and an unpaid invoice, which generates support tickets and tempts someone to loosen the check.

The rejection paths raise rather than return a truthy value. That matters because of how agents read tool results. Agno's implementation shows the pattern of a tool that will not execute until a human confirms, surfacing a structured requirement object to the application (the Agno guide). Returning an error the orchestrator can read, and that the audit log records, keeps the failure visible instead of letting the agent narrate success.

Two-step confirmation without blocking the conversation

A gate that blocks on a human is often too slow for the interaction. The customer is on a chat, the agent needs a yes, and nobody wants a frozen session. The async pattern solves this, and Auth0's write-up is the clearest description: the agent sends a request for authorization, the user is notified on any device, and the agent continues other work until the decision arrives (Auth0).

The sequence for money movement looks like this:

  1. The agent prepares the payment and calls create_confirmation_request with the server-side facts. Nothing moves.
  2. The service issues a token, sends a push or email stating amount, payee and purpose in plain language, and returns a request ID.
  3. The agent tells the customer what has been prepared and what to approve. The conversation continues.
  4. The customer approves in the out-of-band channel. The token becomes redeemable.
  5. The agent, or a scheduled worker, calls execute_payment with the token. The gate runs the checks above.

This maps onto the way escalation is normally run. Zendesk's workflow recipe for an AI agent that collects information and escalates to a human shows the same shape: gather what the human will need, tell the customer what is happening, and hand off with context intact (the Zendesk help article). Your confirmation request is the handoff, with the additional property that the recipient's approval is cryptographically bound to a single payment.

For reviewer-side approvals, the same pattern applies with a role check. The reviewer sees the transaction, the amount, the payee and why the agent proposed it, then approves or rejects. Do not let the reviewer edit the amount in place; a changed amount voids the token and issues a new one. The reason is auditability: the approved artifact and the executed artifact must be byte-identical on the fields that matter.

What to log, and how to handle retries

Every gate decision is an event. The minimum record is the transaction ID, the token ID if one existed, the outcome, the reason code, the subject, the channel and a timestamp. Being able to answer "who approved this payment and on what device" is what turns the gate into evidence rather than a promise.

This is the same discipline used in incident response, where structured logs with correlation IDs and immutable trails let a team reconstruct exactly what happened and when (Cribl on detection and response). Support platforms treat the analogous record as a prerequisite for trust too. Sampled review of deflected conversations, checking whether the customer came back with the same problem, is how support leaders distinguish genuine resolution from a closed ticket (the Swept analysis of deflection metrics). For money, sample the rejected calls: were they genuinely missing a token, or is the gate misfiring on a legitimate flow?

Retries need their own rules, because most retry logic is designed to succeed eventually, which is exactly wrong here.

  • Never retry a rejection automatically. confirmation_required means a human has not said yes. Retrying with a blank token is a loop that either does nothing or, in a badly written tool, eventually finds a path through.
  • Retry only on transient infrastructure errors. A timeout on the ledger write is safe to retry because the token consumption is idempotent. A mismatch is not.
  • Use an idempotency key per transaction, so a retried successful post does not double-debit. The key should be the transaction ID, not a random value generated at retry time.
  • Expire tokens loudly. A token that expires because the customer never approved should close the request and notify the customer, not sit in a queue waiting for an operator to nudge it.

Tying this back to the tier table: tier 1 and 2 approvals should have a stated response window, just as an escalation matrix names the time each owner has to act (Knowmax). When the window passes, the request closes and the customer is told. That is a better outcome than a payment that executes three days later when nobody remembers the context.

An insight worth taking to your design review

The counterintuitive result of putting the gate in the tool is that you can loosen everything else. Because execute_payment cannot run without a matching token, you can let the agent draft freely, propose amounts, and explain options at length without reviewing every message. The agent becomes genuinely useful in preparing the payment while remaining incapable of posting it. That separation, prepare freely and execute only on a bound yes, is more permissive and safer than the common alternative of a cautious prompt plus occasional human review of transcripts.

The second insight is about where teams under-invest. The token verification gets the attention; the audit and retry handling gets skipped. But the failures that reach a customer are usually procedural: a token burned without a post, a retry that double-debits, a rejection nobody investigated. Build the reason codes and the idempotency key first. They are cheaper than a reconciliation project later.

If you are wiring an agent to several underlying model providers and want the same gate across all of them, one API key for text, image and embedding models removes a category of integration work from the critical path: Quanta Inference. Keep the money logic in your own tool either way. The gate belongs where the ledger is.

FAQ

FAQ

Does every refund need a human confirmation?

No. Refunds inside a stated policy, to the original payment method, below a threshold you have documented, are tier 1 and can run on a customer confirmation token without an internal reviewer. What they cannot do is run with no token at all. The rule is about binding the action to a named human who saw the amount and the payee, not about adding a second pair of eyes to every small transaction.

Can the model be trusted to decide when confirmation is needed?

It can propose, but the tier should be attached to the tool and the transaction facts, not chosen by the model at runtime. A model that decides its own approval requirements will eventually decide wrong, and prompt-injected instructions can push it further. Set the tier server-side from amount, payee status and policy, exactly as an approval limit is set in an escalation matrix, and have the tool enforce it.

How long should a confirmation token live?

Long enough for a customer to pick up their phone, short enough that a stolen token is useless. Minutes is usually right for a customer push notification. Reviewer tokens can last longer, but they should still expire on the same schedule as the approval window you have written down, and any change to the amount or payee must void the token immediately rather than extend it.

What happens if the customer says yes but the payment fails downstream?

The ledger write and the token consumption should be in one transaction, so the token rolls back and the customer can approve again, or you can re-issue a new request. What you must avoid is consuming the token before the write, which leaves a burned approval and an unpaid invoice, or posting before consuming, which opens a window for the same token to be replayed.

Is a chat message from the customer enough of a confirmation?

It is evidence of intent but not a control, because it is not bound to an amount or payee and can be generated by the model itself. Use the out-of-band channel, push or email to the address on file, with the amount and payee stated in the request. If you must accept in-chat confirmation, capture it server-side, issue a token from that capture, and never let the model supply the token value.

Sources

Sources

  1. Implement human-in-the-loop confirmation with ...
  2. How to add human-in-the-loop controls to AI agents that ...
  3. Escalation Management: Process, Matrix & Best Practices
  4. Escalation Matrix: Meaning, Levels, Template and Example
  5. Tool calling authentication for AI agents
  6. Secure “Human in the Loop” Interactions for AI Agents
  7. Workflow recipe: Using an AI agent to collect customer info ... - Zendesk help
  8. Implementing Threat Detection and Incident Response ...
  9. The Deflection Rate Dilemma: 5 Ways to Verify AI Help Agent Metrics
About the author

About the author

Antonio Evans
Antonio Evans

Antonio Evans is the founder of Quanta Intelligence and a fractional CTO who builds HIPAA-ready AI systems for health and life-science companies. Before that he was technical co-founder of FriendsEAT for twenty years, CEO of Socialdraft, and a data engineer at IBM. Antonio lives with his dog Santino in Lisbon, Portugal.

All articles