Rightshold™ Sign up

Answers · agent payments

How do I stop two AI agents from paying the same vendor invoice twice?

Last updated

TL;DR: An idempotency key stops the same payment being sent twice when one agent retries. It does not stop a second agent, or a second task, from paying the same vendor invoice as a new payment with its own key. That is the duplicate-bill problem: two processors each treat an invoice as unpaid and each pay it. Catch it before anyone authorizes spend, by claiming a normalized key of vendor id + invoice number + amount + currency in one shared register. Invoice numbers such as INV-001 collide across vendors, which is why the vendor id belongs in the key; amount and currency distinguish a corrected re-issue from a second payment of the same bill. If the key is already taken, refuse to pay and show the earlier claim. Stripe's Refunds API even lists duplicate as a reason you can attach when a second charge already went through. PCAOB AS 1105.11 defines completeness as the assertion that all transactions that should be presented are included, and AS 1105.10 tells the auditor to test the completeness of company-produced information: two unmarked payments of one invoice overstate expenses, and a claim recorded before authorize gives that population something to test. On Rightshold, record_action is keyed by client_event_id on your account's action stream: the same key and the same content come back as a duplicate of the first receipt; the same key with different content is rejected because that key is already bound to different event content. Both agents can attempt the claim with a deterministic client_event_id such as inv:<vendor>:<invoice>:<amount_minor>:<currency> before calling authorize_spend. authorize_spend does not deduplicate by reference — a second call still counts toward the cap — so the claim has to happen first. This page is about two agents (or two tasks) paying the same vendor bill. Stopping one agent from paying twice when it retries, binding a receipt to an idempotency key and reconciling before replay, reserving a shared spend pool, and stopping one agent from both creating and approving a payment are covered on separate pages.

What should I check, step by step?

1. Write the normalization rules down before any agent pays: use your internal vendor id (not the display name), a single rule for trimming and casing invoice numbers, amounts as integer minor units, and currency as an ISO 4217 code.

  1. Build the claim key as vendor + invoice number + amount + currency. Keep it within 160 characters of letters, digits, periods, underscores, colons, slashes, at signs or hyphens, which is what client_event_id allows.
  2. Put the claim in one place every agent's payment path goes through: the same Rightshold account's action stream, or your own invoice register with an atomic insert on that key.
  3. Before authorize_spend, record the claim with record_action (action_type invoice.claimed, client_event_id set to the key) using a canonical body that does not name which agent asked, so two agents sending the same claim look identical. If the receipt comes back with duplicate: true, the invoice was already claimed — do not pay. If the call is rejected because the key is bound to different content, stop and inspect; do not mint a new key.
  4. Only if the claim is new, call authorize_spend and put the same key in reference so the spend receipt points at the claim. authorize_spend does not treat reference as a dedupe key; each new allow counts toward the daily cap.
  5. After the payment succeeds, record payment.sent under a new client_event_id (for example pay:<claim-key>) that names the claim, the spend receipt id and the provider payment id.
  6. If you allow partial payments against one invoice, drop amount from the uniqueness key and track the remaining balance in your register. When amount stays in the key, two identical full-amount payments of the same invoice are the duplicates you catch; two legitimate partials of different amounts will not collide.
  7. If a duplicate payment already went out, refund it on the rail (Stripe's Refunds API accepts reason=duplicate) and post a linked reversing entry for the extra payment — don't delete that record. How to record a void or reversal of an AI agent's payment on an append-only ledger covers that pair.

Why isn't two AI agents paying the same vendor invoice the same as a payment retry?

A retry is one intended payment that may be sent more than once (a timeout, a crashed worker, a framework replaying a step). The fix is one idempotency key created before the first attempt and reused unchanged, which is covered on the page about stopping an agent from paying twice when it retries, and on the page about binding a payment receipt to its trigger, policy and idempotency key before replay.

How do I dedupe vendor, invoice number, amount and currency before an AI agent authorizes a payment?

Vendor id first. Many vendors number invoices from 1 or INV-001, so invoice number alone collides. Use the identifier from your vendor master, normalized the same way every time (lowercase internal id, not 'ACME Inc.' vs 'Acme, Inc.').

What does Rightshold do, and not do, to stop two AI agents paying the same invoice?

record_action keys events by client_event_id on your account's private stream and will not bind that key to different content. authorize_spend checks one credential's spending policy and records a signed allow or deny; it does not look up other agents' payments or treat reference as unique. An allow counts toward the daily cap when it is recorded.

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: “An agent wants to pay vendor acme-supplies invoice 4471 for 150.00 USD. The invoice claim key is free. Check this purchase against my spending policy and record the decision, with that invoice key as the reference.”

One-click setup

{
  "tool": "authorize_spend",
  "arguments": {
    "credential_id": "crd_YOUR_AGENT_CREDENTIAL",
    "amount_minor": 15000,
    "currency": "USD",
    "merchant_domain": "api.acme-supplies.example",
    "reference": "inv:acme-supplies:4471:15000:USD"
  }
}

Rightshold records invoice-claim events you send and signs spend allow/deny decisions per agent credential. It does not parse invoices, keep a vendor master, treat authorize_spend's reference as a unique invoice key, refund duplicate charges or connect to your accounts-payable system. Normalization, the shared claim and any refund are steps you run. Not accounting, tax or legal advice.

What are the sources?

Related

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}}'