Answers · agent payments
How do I enforce segregation of duties so an AI agent cannot both create and approve a payment?
Last updated
TL;DR: Split create and approve across different principals with different credentials: the agent (or human) that drafts or submits a payment request must not hold the credential that can approve or settle it. Bind the request to a create_principal_id and a separate approve_principal_id; record both identities on an append-only ledger the agent cannot rewrite; and at settlement re-check that the approver is not the creator and that authorize_spend still matches the approved action. That is segregation of duties for money moves — distinct from four-eyes (two independent yeses on one action), quote-bound approval (binding a yes to a quoted amount), and named-approver pages (recording who said yes). Here the control is the role split itself: create vs approve cannot be the same principal. request_human_approval opens the approval for a non-creator; verify_human_approval and authorize_spend gate settlement; ledger_verify keeps both identities checkable.
What should I check, step by step?
1. Name the two roles for every money move: creator (drafts or submits the payment request) and approver/settler (may authorize release). Write that the same principal_id and the same credential must never hold both.
- Issue separate credentials: an agent key that can call request_human_approval (or create a pending payment) but cannot call decide_human_approval or hold the owner/approver key; an owner or second-agent key that alone can decide and that authorize_spend will accept for settlement.
- Put create_principal_id (and optional create_credential_id) inside the action object the approval hashes. Reject any decide_human_approval where the approver equals the creator, and reject any settlement where the settler equals the creator.
- Show the approver the same payee, amount, currency, rail and memo the creator submitted — not a rewritten chat summary. Hash those fields (RFC 8785 JCS then sha256) so a quiet change fails verify_human_approval.
- At release, recompute the action hash, verify_human_approval against it, confirm approver_id ≠ create_principal_id, then call authorize_spend under the approver/settler credential only.
- Write create and approve events as separate ledger entries (who, when, action hash, decision). The agent's API key must not UPDATE or DELETE either row.
- Drill it: attempt self-approval with the agent key, attempt approve-with-creator-id, change amount between create and approve, and confirm every path denies.
- Keep vendor or bank-detail changes on a third path or under the approver role — a creator that can also re-point payout accounts defeats the split.
Why create and approve must be different principals
If one agent credential can both queue a payment and mark it approved, a prompt injection, a fake invoice, or a compromised key completes the whole path alone. Segregation of duties (SoD) exists so no single actor finishes a sensitive money move by itself.
- NIST SP 800-53 Rev. 5 AC-5 (separation of duties) is the control language: divide duties so the person or process that initiates a transaction is not the one that authorizes it. For agents, that means separate principal ids and separate credentials, not two prompts in one session.
- How to keep segregation of duties when an AI agent pays bills covers accounting-system roles (clerk vs approver vs payer in QuickBooks). This page is the create-vs-approve credential and ledger split for any agent payment path, including wallets and APIs outside the books.
How this differs from four-eyes, quote-bound and named-approver controls
Four-eyes requires two independent humans on one action hash — still useful for high-impact moves, but it does not by itself stop the creator from being one of the approvers unless you also enforce the role split.
- Quote-bound approval binds a yes to a quoted amount and payee so settlement cannot drift. Named-approver pages record which human said yes and which policy version. Keep those; add SoD so the creator credential is never eligible to approve or settle.
- request_human_approval hashes the action you send (include create_principal_id); decide_human_approval needs the owner or designated approver key and refuses delegated agent keys that would let the creator approve itself; verify_human_approval is free and fails when the bound fields no longer match.
Recording both identities and gating settlement
Ledger both events: payment_request_created (create_principal_id, credential_id, action hash, amount, payee) and payment_approved or payment_denied (approve_principal_id, decision, same action hash). Denials matter as much as approvals.
- authorize_spend should run only under the settler/approver credential after a matching verified approval. set_spend_policy on that credential still caps per-tx and daily totals so a wrong yes cannot empty the wallet.
- ledger_verify is free and keyless: it recomputes every payload, leaf and block hash so an edited create or approve entry shows up. Pair with record_ai_run when you need the model output that produced the draft tied to the same trail.
How do I ask Rightshold about this (free, no key)?
Add the MCP server https://www.rightshold.com/mcp to Claude, Cursor, ChatGPT, Grok or VS Code, then ask: “My purchasing agent drafted a payment. Open a human approval for the exact payee, amount and rail, bound to create_principal_id of the agent, and refuse any approval or settlement from that same principal.”
{
"tool": "request_human_approval",
"arguments": {
"summary": "SoD create≠approve: pay vendor V-8842 420.00 USD (ACH …3210) drafted by agent crd_buyer",
"action": {
"type": "pay",
"vendor_ref": "V-8842",
"amount_minor": 42000,
"currency": "USD",
"rail": "ach",
"account_last4": "3210",
"create_principal_id": "agent:buyer-bot",
"create_credential_id": "crd_buyer",
"rule": "create-vs-approve",
"policy_id": "ap-sod",
"policy_version": "v2"
},
"expires_in_minutes": 60
}
}- Needs your API key. decide_human_approval (owner or designated approver account key) signs the yes; delegated agent keys that match the creator are refused so the drafter cannot approve itself.
- verify_human_approval is free: re-check the receipt against the same action (including create_principal_id) right before settlement; fail if the approver equals the creator or the hash drifted.
- authorize_spend under the approver/settler credential returns a signed allow or deny against that credential's spend policy — never under the creator key.
- ledger_verify is free and keyless for the create and approve entries.
Rightshold records approval requests, owner decisions and signed spend allow/deny decisions on a hash-chained ledger. It never holds or moves money and does not run your identity provider. Enforcing that the creator credential cannot call decide_human_approval, and that settlement refuses creator==approver, is policy you configure around those receipts. Not financial or legal advice.
What are the sources?
- NIST SP 800-53 Rev. 5: AC-5 (separation of duties) and AC-3 (access enforcement)
- RFC 8785: JSON Canonicalization Scheme (JCS)
- RFC 6962: Certificate Transparency (append-only Merkle hash logs)
- EU AI Act, Regulation (EU) 2024/1689: Article 14 (human oversight)
- Human approvals on Rightshold
- Agent spending policies on Rightshold
Related
- How to keep segregation of duties when an AI agent pays bills
- How to require two independent approvals before an AI agent acts
- How to require quote-bound approval before an agent payment settles
- How to record a named approver and policy version in a tamper-evident entry for every agent approval
Not legal advice. JSON · Markdown · All answers
Get a free API key in one call
No card and no email needed. 100 free credits; the license and policy checks stay free without a key.
curl -s -X POST https://www.rightshold.com/v1/accounts/signup -H "Content-Type: application/json" -d '{"accept_terms":true}'
export RIGHTSHOLD_API_KEY=rs_... # the api_key from the reply (shown once)
curl -s https://www.rightshold.com/v1/licenses/check -H "Authorization: Bearer $RIGHTSHOLD_API_KEY" \
-H "Content-Type: application/json" -d '{"license":"MIT","intended_use":{"commercial_use":true}}'
